English | 日本語
terraform plan: no changes. Reality: someone edited 3 resources in the AWS console last week. SyncVey catches what plan can't.
SyncVey organizes your AWS resources into a System → Environment → Asset hierarchy,
flags configuration drift between your tfstate and live AWS state, and tracks application
metadata (language, framework, dependencies) per environment.
Spin it up with a single docker compose up.
Live feature tour → https://syncvey.com/ Development history → https://syncvey.com/history.en.html — every merged pull request in order, with dates and sizes measured from git.
Dashboard — a hero-signal row answers what's happening (drift / EOL), whether it's urgent (trend vs the last scan, freshness), and what to do next
Drift report — attribute-level diff between tfstate and actual AWS state
Drift history — drift trend over time, with the field-level diff for each snapshot
I'm 62, and I still write code. For years I watched infrastructure teams keep their AWS inventory in spreadsheets — and every single time, without exception, I watched those spreadsheets rot.
One day I asked the obvious question: we have tfstate, we have boto3 — why are we still doing this by hand?
The other thing nagging at me was middleware EOL. We had a list of things to watch, but no alerts, no dashboard, nothing that pointed to a next action — just another spreadsheet quietly going stale.
So I built the tool I'd always wanted. Scheduled scans, drift detection, EOL alerts — in one place, self-hosted, with your data staying inside your own infrastructure.
Each tool in this space does one slice well. driftctl pioneered Terraform drift
detection, but it's a stateless CLI — and is no longer maintained since Snyk archived it.
Steampipe turns your cloud into queryable SQL, which is excellent for ad-hoc
investigation, but it's a query engine you build dashboards on, not a turnkey ledger.
Cloud Custodian shines at policy-as-code enforcement (tag, stop, remediate), yet it's
about reacting to rules, not giving you a browsable inventory. SyncVey sits in the middle
as a self-hosted web app: a persistent asset ledger with a UI, attribute-level
drift between your tfstate and live AWS — including resources created by hand in the
console that terraform plan never sees — plus a layer none of the others touch:
per-environment application & middleware tracking with EOL alerts. One
docker compose up, and your data stays in your own infrastructure.
| tfstate drift | Detects console-made resources | Ledger + UI | App/middleware + EOL | Form factor | |
|---|---|---|---|---|---|
| driftctl | ✅ | ✅ | ❌ | ❌ | CLI |
| Steampipe | ✅ (query) | ❌ | CLI + SQL | ||
| Cloud Custodian | ❌ | ❌ | ❌ | Policy engine | |
| SyncVey | ✅ | ✅ | ✅ | ✅ | Self-hosted web app |
| Feature | Description |
|---|---|
| Asset ledger | Inventory and search EC2, ECS, Lambda, RDS, DynamoDB, ElastiCache, EFS, EKS, S3, ALB, VPC, EBS, SNS, SQS, API Gateway, Secrets Manager, CloudFront, Route 53 resources — and more |
| AWS scan | Auto-discover 18 resource types — compute, database, storage, network, messaging — in target accounts via AssumeRole |
| Terraform integration | Import assets by uploading a tfstate file |
| Drift detection | Spot attribute-level differences between tfstate and live AWS state |
| Auto Scaling-aware drift | Instances an Auto Scaling group launched or terminated are churn, not drift. SyncVey reads the aws:autoscaling:groupName tag it already scans — no extra API call or IAM — and keeps scale-out/in out of the drift count, shown transparently in their own section. Attribute changes on a persistent instance are still real drift. Toggle with DRIFT_SUPPRESS_AUTOSCALING |
| Deleted-resource detection | A resource that vanishes from AWS is flagged in the ledger and reported as removed drift — the row is kept, not deleted, so you can still see what was there and when it went. Marking only happens for the regions and resource types that scanned cleanly, so a throttled API call or an expired credential can never be mistaken for a mass deletion. If the resource comes back, the flag clears itself. Auto Scaling scale-in counts as churn, not drift |
| Drift history | Track drift over time — every scan/import records a snapshot, with a trend chart and per-snapshot diff |
| Drift risk & attribution | Grade drift by security impact (e.g. a security group opened to 0.0.0.0/0) and trace who changed a resource via CloudTrail |
| Blast radius (optional plugin) | Walk the resource reference graph outward from each drift and rank every resource it can reach by severity-weighted, distance-decayed impact. Richer with the drift-risk plugin installed; detachable — remove the app and the feature disappears |
| Secret rotation drift | Flag Secrets Manager secrets that should have rotated but didn't — rotation disabled, never run, or past the SECRET_ROTATION_MAX_AGE_DAYS limit (default 90). Graded on current state, so it surfaces even when nothing changed between two scans. Metadata only: the secret value is never read or stored |
| Drift briefing | Optional weekly Slack rollup per system — severity counts, week-over-week trend, and the top risky changes with who made them (opt-in via DRIFT_DIGEST_ENABLED) |
| Command line (optional plugin) | Drive scan and drift from a terminal or CI with manage.py syncvey scan / drift / status — the same engine the dashboard uses. drift --exit-code fails the build on any drift; --format json feeds a pipeline. Detachable — remove the app and the command disappears |
| Application tracking | Record language, framework, deployment method, and dependencies per environment |
| EOL alerts | Flag end-of-life middleware/runtimes (offline by default; optional daily refresh) |
| Architecture diagram | Visualize resource relationships within an environment |
| Multi-account | Register an IAM Role ARN per system to manage multiple AWS accounts |
| Layer | Technology |
|---|---|
| Backend | Python 3.12 / Django (server-rendered, no SPA) |
| Frontend | htmx 1.9 + Tailwind CSS — Django templates, no build step, no Node toolchain |
| Database | PostgreSQL 18.3 |
| AWS SDK | boto3 (cross-account access via AssumeRole) |
| Auth | TOTP two-factor authentication (pyotp) |
| Scheduler | django-apscheduler (periodic scans) |
| Infra | Docker Compose |
- Docker and Docker Compose
cp .env.example .envEdit .env:
# Django
SECRET_KEY=your-secret-key
DEBUG=True
# Database (match docker-compose.yml)
DATABASE_URL=postgres://user:password@db:5432/asset_manager
# AWS — IAM user credentials for the central account
AWS_ACCOUNT_ID=123456789012
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_SCAN_REGIONS=ap-northeast-1,ap-northeast-3Production note: set
DEBUG=Falsefor any non-local deployment, and use a strong, uniqueSECRET_KEY.
docker compose up -dThis pulls the published image (jiniie/syncvey, amd64 and arm64) — there is nothing to build and nothing to compile.
Building from source instead: copy
docker-compose.override.yml.exampletodocker-compose.override.yml. It builds the image from your working tree and mounts the tree for live reload — that is the development setup.Pinning a version: set
SYNCVEY_IMAGE=jiniie/syncvey:0.2.0in.env.
Tip: If you use VS Code, open the repo in Dev Containers — the environment is built automatically.
docker compose exec app python manage.py migrate
docker compose exec app python manage.py seed # optional: loads sample data| Service | URL |
|---|---|
| App | http://localhost:8000/ |
| Django Admin | http://localhost:8000/admin/ |
Run a real scan — ledger, drift report, dashboard — against a local AWS emulator (LocalStack). No AWS account, no credentials, no bill.
1. Uncomment the demo block in .env:
AWS_ENDPOINT_URL=http://localstack:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test2. Start the stack with the emulator, create some fake resources, and scan them:
docker compose --profile demo up -d
docker compose exec app python scripts/seed_localstack.py
docker compose exec app python manage.py syncvey scan --system "Demo System"Open http://localhost:8000/ and the scanned resources are in the ledger.
3. Make something drift, then look again:
docker compose exec app python -c "
import boto3
s = boto3.Session(region_name='ap-northeast-1')
q = s.client('sqs')
q.delete_queue(QueueUrl=q.get_queue_url(QueueName='demo-jobs')['QueueUrl'])
ec2 = s.client('ec2')
ec2.stop_instances(InstanceIds=[ec2.describe_instances()['Reservations'][0]['Instances'][0]['InstanceId']])
"
docker compose exec app python manage.py syncvey scan --system "Demo System"
docker compose exec app python manage.py syncvey drift --system "Demo System"Demo System / Production: 2 drifted (changed=1 added=0 removed=1)
~ EC2 i-9538ffc109d6e7b6b (i-9538ffc109d6e7b6b)
instance_state: running -> stopped
- SQS demo-jobs (arn:aws:sqs:ap-northeast-1:000000000000:demo-jobs)
LocalStack's free edition implements a subset of AWS, so 10 of the 18 scanners return data. The other 8 report an error for their service and the scan carries on — a scanner that failed is deliberately excluded from deleted-resource detection, so a service SyncVey could not read is never mistaken for a fleet someone deleted.
| Scans against LocalStack | Paid LocalStack tier only |
|---|---|
| EC2, VPC, EBS, S3, Lambda, DynamoDB, SNS, SQS, Secrets Manager, Route 53 | RDS, ECS, ALB, ElastiCache, EFS, EKS, API Gateway v2, CloudFront |
- "Who changed this?" does not work here. Attribution reads CloudTrail event history, which the free edition does not provide, so drift rows show no actor.
- The image tag is pinned to
localstack/localstack:4on purpose.:latestnow requires a LocalStack auth token and exits immediately without one.
To point SyncVey back at real AWS, comment those three lines out again and run docker compose up -d.
Create a read-only IAM Role in each target AWS account and register its ARN in SyncVey.
Full instructions → aws-setup.md
Quick overview:
- Add your central account's IAM credentials to
.env - Create the
SyncVeyReadOnlyrole in each target account using the provided IAM policy (iam/iam-policy.json) - Register the Role ARN using the 🛡 button on the system card
- Click ScanLine to run the first scan
SyncVey is fully self-hosted. The only outbound connections it makes are listed below — there is no telemetry or usage analytics.
| Destination | When | Direction | How to control |
|---|---|---|---|
| AWS APIs (boto3 / AssumeRole) | On-demand or scheduled scan | Outbound HTTPS | Only when a Role ARN is configured |
AWS CloudTrail (LookupEvents) |
Clicking "Who changed this?", or the weekly drift briefing | Outbound HTTPS | Lazy, never automatic — needs the role + cloudtrail:LookupEvents |
hooks.slack.com |
Drift detected, or the weekly drift briefing | Outbound HTTPS | Per-system Slack Webhook URL (opt-in); briefing also needs DRIFT_DIGEST_ENABLED=true |
endoflife.date |
Daily EOL data refresh | Outbound HTTPS | Off by default — enable with EOL_REFRESH_ENABLED=true |
EOL refresh detail. End-of-life detection works offline using built-in data.
Set EOL_REFRESH_ENABLED=true to enable a daily job that pulls fresh data from
endoflife.date (falls back to built-in data on failure).
By default, only dependencies you actually track are refreshed; set
EOL_REFRESH_DYNAMIC=false to pin it to a fixed known set.
You can also trigger a refresh manually:
docker compose exec app python manage.py refresh_eol --forceSyncVey is a server-rendered htmx app, not a JSON REST API. All views return rendered HTML (full pages or partials).
/ Dashboard
/systems/ Systems
/systems/<id>/environments/ Environments under a system
/systems/<id>/applications/ Applications under a system
/environments/<id>/scan/ Run an AWS scan
/environments/<id>/drift/ Drift report
/environments/<id>/drift/history/ Drift history (trend over time)
/drift-risk/ Drift risk & attribution (security triage)
/blast-radius/ Blast radius (drift impact across the reference graph; optional plugin)
/drift-digest/ Drift briefing (weekly Slack rollup preview)
/environments/<id>/diagram/ Architecture diagram
/environments/<id>/sync-s3/ Sync remote tfstate from S3
/assets/ Asset list
/assets/<id>/ Asset detail
/upload-tfstate/ Import assets from tfstate
/samples/ Sample library
/audit-log/ Audit log
/profile/ Profile / 2FA settings
/admin/ Django Admin
Full route list: asset_manager/urls.py
The syncvey_cli plugin adds a syncvey management command so an operator — or
a CI pipeline — can trigger a scan and read drift without opening the web UI. It
drives the same scan/drift engine the dashboard does, so it can never disagree
about what counts as drift.
# Run a live AWS scan and record a drift snapshot (all systems, or one)
docker compose exec app python manage.py syncvey scan --system e-commerce
# Print the current drift; add --format json to feed a pipeline
docker compose exec app python manage.py syncvey drift --env prod
# Fail the build on any drift — drop this into a CI step
docker compose exec app python manage.py syncvey drift --exit-code
# List systems / environments with asset counts and last-scan time
docker compose exec app python manage.py syncvey statusExit codes are the CI contract: 0 = ok / no drift, 1 = drift found
(only with drift --exit-code), 2 = a scan job failed or the selector
matched nothing. Detachable like the other plugins — remove
syncvey_cli from INSTALLED_APPS and the command disappears.
System
└── Environment (PROD / STG / DEV / QA)
└── Asset (asset_type + category; raw attributes stored in a JSON field)
└── Application
└── AppEnvConfig (per-environment settings)
└── AppDependency
# Open a shell inside the app container
docker compose exec app bash
# Generate migrations after model changes
docker compose exec app python manage.py makemigrations
# Tail logs
docker compose logs -f app
docker compose logs -f db| Releases and changes | CHANGELOG.md |
| Reporting a vulnerability | SECURITY.md — please don't use a public issue |
| Contributing | CONTRIBUTING.md |



