/ Docs Guides / Migrating from AWS Quickstart ← All guides
Migration

Your habits mostly work. Where they don't, we say so first.

The rule this page is written under: for every AWS capability, IG1 either matches the behaviour, ships a mapped path, or explains the difference before you hit it — silence is the only failure mode. Four grades below: drop-in (AWS tooling works after ≤3 lines of config), familiar (same mental model, renamed knobs), different-by-design (we deliberately diverge, with the why), and gap (does not exist here yet — stated up front, not at delete time).

1 · The fidelity table

You know it asGradeThe one-line story
EC2 instancesfamiliarVerbs map 1:1; identity and root disk survive stop/start. Stopped bills no compute since 2026-08-30 (the meter follows power state — the invoice line shows stopped time as storage-only, and attached volumes keep billing, exactly like AWS), and resize parks in a confirm step (confirm_resize/revert_resize) AWS does not have.
AMIs / CreateImagefamiliarImport is easier than AWS — qcow2 straight from a URL. But snapshots are crash-consistent by default (stop first if that matters), and deleting an instance-snapshot image can destroy the only copy of that disk.
Key pairsdrop-in (CLI)create/import/delete match AWS. Agents may only import public keys — deliberate, see §4.
Instance typesfamiliarCurated grid, no custom shapes — and the m/r RAM ratios are inverted vs AWS. Read §2 before you pick anything.
EC2 Auto ScalingfamiliarGroups with min/max/desired, alarm-driven scaling, self-healing, drain-first scale-in, LB member registration — live on the factory API. Absent: target tracking, scheduled/predictive policies, launch-template versioning. Elasticity guide.
EBS volumesfamiliarGrow-only resize matches. There is no IOPS/throughput dimension at all — one shared-Ceph class; if you run io2-sensitive workloads, raise it before migrating, not after.
EBS snapshotsfamiliar, one loud caveatFat-finger protection, not DR: same Ceph cluster as the source, no cross-site copy. Free, but pooled into the same GB quota as volumes.
S3drop-inReal SigV4, per-tenant keys: aws-cli and boto3 work against the IG1 endpoint after the three config lines in §3.
RDS PostgreSQLfamiliar (durability since 2026-08-29)replicas 1–3 with in-place scaling (synchronous replication, operator failover) and a backup block: daily base backups plus continuous WAL archiving into the platform's object store under a prefix only your tenant can write — point-in-time recovery, with the window shown on the detail. Deleting an instance removes its schedule and leaves the archive; that is the point of DR. Proven live: a marker row written, backed up, restored into a throwaway cluster and read back (make validate-52).
RDS MySQL / MariaDB / SQL ServergapPostgres and Kafka are the only managed engines. "MySQL on a VM" is the path, and it gates which workloads can migrate — decide up front.
MSK (Kafka)familiarSingle-node KRaft, plaintext 9092, no auth: strip TLS/SASL from client configs; isolation is network-level. The API says credentials: null and means it.
VPC / subnetsfamiliarFused network+subnet resource, IPv4-only, strict CIDR validation (friendlier than Neutron or AWS).
Internet / NAT gatewaydifferent-by-designThere is no tenant IGW, ever, and no self-service SNAT egress. Public ingress is self-service: a floating IP from the public pool (since 2026-08-26, direct L4 like an EIP), or the managed .75 edge (per-hostname exposures, or an internet-facing load balancer — zero IPv4 spent). §4 has the why; the networking guide has the how.
Security groupsfamiliarNear-1:1 including allow-all default egress; default-deny ingress is deliberate (§4). One CIDR per rule, no -1 protocol shorthand.
Elastic IPsdrop-in verbsallocate/associate/disassociate/release map exactly — and since 2026-08-26 a floating IP is a public address by default (185.255.84.64/26): reachable from the internet like an EIP, billed per hour. The name-based HTTPS publish path (a hostname with TLS, zero IPv4) stays the edge's job.
NLBfamiliar (L4)One composite call builds what takes AWS four; health monitors are on by default, member health is readable. The scheme maps too: internal is the default, internet_facing=true is AWS's internet-facing — the VIP is published through the .75 edge as {name}-{tenant-slug}.10.57.8.75.nip.io (TCP listeners; pending until the edge syncs; internal-CA certificate on that generated name — attach your own domain to get a publicly-trusted one). One thing AWS never asks of you: themembers must serve TLS on the member port, because the edge re-encrypts to the VIP — a plain-HTTP backend fails the handshake (a self-signed certificate is fine; the edge does not verify it). And one thing it never made you think about, which now behaves the same here: the listener port is open when the load balancer is. OVN gives the VIP port the tenant's default (deny-all) security group, so a load balancer came up ACTIVE with ONLINE members and answered nothing; the composite now creates {name}-lb, opens each listener port on it from 0.0.0.0/0 — the boundary is the network, not the rule — and attaches it to the VIP port beside anything already there, deleting it with the load balancer (only that exact name on that VIP port; a group you attached is never touched). manage_security_group=false opts out and says what it costs: the VIP refuses traffic until you attach a group yourself. No TLS termination at the LB, no ALB/L7 — certificates live on backends or at the edge.
Route 53familiar subsetA/AAAA/CNAME/TXT/MX recordsets on all four surfaces (ig1 dns, ig1_dns_zone/ig1_dns_record, console, MCP) — so a Terraform-heavy Route 53 migration ports as Terraform. No UPSERT, trailing dot required, and no ALIAS/latency/geo/health-check routing: this is authoritative records, not Route 53's traffic-management half. Zones resolve publicly since 2026-08-25 — delegate to ns1/ns2.cloud.ig1.com, paste your existing records into POST /v1/dns/zones/{zone_id}/import (dry-run first), and let GET /v1/dns/zones/{zone_id}/delegation tell you whether the internet followed rather than guessing. Both are agent tools too (get_dns_delegation, import_dns_records), so an agent can drive the whole move and verify it landed.
EKSfamiliarCluster = control plane + one worker pool; deletion protection nearly exact; worker autoscaling via annotations. Upgrade replaces workers together with the CP (EKS lets nodes lag). type=LoadBalancer Services currently pend forever — check before you helm-install.
IAM + STSdifferent-by-designTiers 0/1/2 instead of policy JSON. The flagship difference — §4 first paragraph.
Secrets Managerfamiliar core, one postureProject-scoped secret store (Barbican) at /v1/openstack/key-manager/v1/secrets — store, list, metadata, delete on every surface. No versions, no rotation lambdas: rotation is create-new → repoint → delete-old, by hand or by agent. The posture: agents can run the whole lifecycle but can never read a value back — payload reads are portal/CLI reveal-once only, because a value in a transcript outlives every rotation. Consumers fetch at deploy time with their own credential, exactly as with Secrets Manager.
CloudTraildifferent-by-designWrite-side: durable (the api.audit Kafka topic, 30-day retention), values-redacted (parameter names only). Read-side: GET /v1/audit answers from the serving pod’s ring when that ring provably covers your window, and from the durable topic when it does not — every response says which (source), and a redeploy no longer resets the trail (phase 54, 2026-08-30).
CloudWatchfamiliar (alarms) / gap (history)Alarm states match (ok / alarm / insufficient_data), transitions-only events. Metrics are on-demand samples — no time-series store, no dashboards over history.
Budgets / Cost ExplorerfamiliarEUR, integer cents, per-second units. amount_cents: "100" means €1 — a ported €100 budget silently becomes 100× smaller. Honest linear forecast, one absolute threshold, event-only notification.
Service Quotasdifferent-by-designTiers, not per-quota requests — and a refusal can mean the hardware is genuinely full (§4).
SNS / EventBridgefamiliar, one trapSubscriptions persist across redeploys (since 2026-08-21 — they were per-pod before, which meant a create 404'd about half the time and every roll silently deleted every customer's webhooks). The trap that remains: the HMAC key is sha256_hex(secret), not the raw secret — Stripe-style verifiers fail silently until you use the documented snippet. Delivery is 3 attempts (1 s/5 s/30 s backoff, 5 s timeout) with a per-webhook delivery log at GET /v1/webhooks/{webhook_id}/deliveries and a signed ping at POST /v1/webhooks/{webhook_id}/test. That log is the one part still in memory and per-replica — it is a recent tail, not an audit trail, and it says so. Private destinations (loopback, link-local, RFC1918, CGNAT) are refused at create and re-checked before every attempt, so an SSRF-by-DNS-rebind cannot sneak through.
aws CLIfamiliarNoun-tree verbs, same JMESPath engine, lowercase keys. No auto-pagination (>1000 resources truncate — page yourself), no waiters, and destructive verbs exit 3 without --yes (put IG1_YES=1 in CI).
Terraform (hashicorp/aws)familiar, landminesImport-by-id works on every resource. But ig1_server.flavor_id is still replace-on-change where AWS updates in place — a plan that shows -/+ destroy and then create replacement on a flavor bump will delete the instance, so resize out-of-band (ig1 compute instance resize) until the provider learns the in-place path. ig1_database.size_gb was the same trap and was fixed on 2026-08-26: it now resizes in place through PATCH /v1/databases/{engine}/{name}, and the API's refusal to shrink reaches you as an error instead of as a smaller, empty instance. ig1_volume.size_gb grows in place. One env file now drives both (2026-08-22): IG1_API_KEY may be the joined id:secret the CLI has always taken, or the bare id with IG1_API_SECRET beside it, and the CLI and the provider resolve either spelling identically. Setting a joined pair AND a different IG1_API_SECRET is refused rather than guessed — silently picking one is how a credential gets used where you did not mean it to be.
boto3 / SDKsdrop-in (S3) / familiar (all three SDKs)For object storage: keep boto3, point it at our endpoint. For the control plane: use the generated IG1 SDKs. Python ships the auth shim — from ig1_api.auth import token_from_env, then cfg.access_token = token_from_env(), reading IG1_API_KEY in either the joined id:secret or split IG1_API_SECRET spelling. Go and TypeScript ship the same shim since 2026-08-31 — ig1api.TokenFromEnv() and tokenFromEnv() respectively — one contract in all three languages, each proven live by its own compatibility suite.

2 · The flavor trap: our m is your c

IG1's flavor families predate this guide and their letters do not mean what AWS's do: IG1 m1 is 2 GiB/vCPU (AWS's c ratio) and IG1 r1 is 4 GiB/vCPU (AWS's m ratio). Mapping name-to-name — m5.large → m1.large — halves your RAM, and the failure arrives later, as OOM kills under production load. Map by ratio:

Coming fromPickWhy
AWS m5.* (general purpose, 4 GiB/vCPU)IG1 r1.*same RAM ratio
AWS c5.* (compute, 2 GiB/vCPU)IG1 m1.*same RAM ratio
AWS r5.* (memory, 8 GiB/vCPU)operator requestno shipped 8 GiB/vCPU family — ask before migrating the workload

The portal's flavor picker carries the same warning at both points of contact (launch wizard and resize drawer), so nobody has to remember this page at 2am.

3 · S3: already compatible — the whole layer is three config lines

Object storage speaks real SigV4 with per-tenant keys: versioning, multipart, presigned URLs, lifecycle/CORS all work from stock AWS tooling. The entire compat layer:

aws configure set default.s3.addressing_style path
export AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED
aws s3 sync ./data s3://mybucket --endpoint-url https://s3.cloud.ig1.com

Quote your access key, or use a profile. A tenant key has the tenanted form tenant$<project>-…, and $ is an expansion character in sh, bash and zsh — an unquoted export truncates the key before the AWS CLI sees it, and the request is refused as InvalidAccessKeyId. The console's Object storage page hands you a ready ~/.aws/credentials profile, which goes through no shell at all; if you prefer environment variables, single-quote them.

One rough edge worth knowing before you hit it. This gateway is Ceph RGW, and RGW returns its errors with an empty <Message> element. The AWS CLI (v1 and v2 alike) crashes on that while formatting the error, printing argument of type 'NoneType' is not a container or iterable instead of the real problem — so a wrong key, a missing bucket and a denied request all look identical, and none of them look like what they are. It is a display bug in the CLI, not a failure of the request: the SDKs are unaffected and report the true error code. If the CLI prints that line, re-run the same call with boto3 (or any SDK) to see what actually happened.

Path-style addressing is a permanent stance (virtual-hosted names 404 on the exact-host ingress), the checksum variable works around aws-cli ≥ 2.23's CRC32 default against this RGW, and bucket names transfer as-is — the namespace is per-tenant, so nobody can squat yours. One re-architecture note: there are no public buckets; anything you served world-readable moves behind a presigned URL or the edge.

4 · Deliberate differences — read once, save a week

These are product decisions, not gaps, and they do not get diluted. Each paragraph is the "coming from AWS" briefing for one of them.

Tiers instead of IAM policy. IAM makes you author policy to become safe; IG1 makes you ask for power to become dangerous. Every credential carries a tier: tier 0 reads everything in its tenant, tier 1 creates and changes, tier 2 destroys. Least-privilege is the default instead of a 40-line JSON document you have to get right; escalation is a human act — tier 2 is never agent-mintable, a credential can never outrank its creator, and revocation kills the credential and every token derived from it within a minute. What you lose is resource-grain policy — you cannot express "read S3 only" (separate S3 keys partially cover it). Budget for three things: replacing IRSA (there is no workload identity yet — the PAT-in-Secret recipe is the interim), replacing AssumeRole chains (one tenant = one account; use org users + tiers), and remembering that authorization is always tenant and tier — a role check alone is never enough here.

Default-deny ingress. Egress matches AWS (allow-all); a new group allows no ingress at all, and an instance without explicit rules is unreachable on purpose. This is the #1 first-day question — the answer, including the aws-like cutover template, is in the networking guide. World-SSH (0.0.0.0/0 on 22) is refused unless you say allow_ssh_from_anywhere=true — the escape hatch is right there, but the error text will tell you a real CIDR is the better fix.

There is no internet gateway, and you cannot make one. Two production outages taught us that a second gateway-bearing router on shared fabric takes down customers who aren't ours. Tenant routers cannot hold an external gateway — the API 403s with the full story, and the agent tool does not even have the parameter. North-south is platform-provided: public ingress is a self-service edge exposure (one call, one hostname, TLS at the edge) — requested for any address you own, or created for you when a load balancer is marked internet-facing; outbound SNAT to the internet is not self-service. "Internet-facing" carries a bound worth reading before you plan a cutover: it means reachable from outside the tenant through the platform edge at 10.57.8.75, published under a generated .nip.io hostname — and that hostname resolves to a private address, so on its own the flag buys you fabric/VPN reach, not the internet. The edge does have a public face (185.255.84.178, since 2026-08-25), and the way to reach it is to attach a domain you own: same VIP, public name, publicly-trusted certificate, no lab CA for your visitors. Plan a cutover on the custom domain, not on the generated hostname. Your east-west networking is entirely yours; the internet boundary is entirely ours — what you place in its doorway is your call.

Quotas are tiers, and "no" can mean the hardware is full. There is no per-quota increase request; capacity comes in tiers and moving up is an operator action. Occasionally you will see a refusal AWS users have never seen: the platform's sellable budget is exhausted. AWS hides that problem because their capacity is effectively infinite from your seat; ours is real hardware in a real rack, and we think telling you the truth beats overselling and failing your launch at 2am.

Agents operate under guardrails humans do not have. The MCP surface will not generate key pairs (public-key import only) and will not rotate database passwords (that breaks every connected application — it stays a human act with a typed confirm). Agents can read S3 and database credentials and mint scoped API keys — with limits the API itself does not impose: minted keys are tier ≤ 1 with mandatory expiry ≤ 7 days, secret-reading tools warn that the value enters the transcript, and everything that persists is write-ahead audited and refuses to act unattributed. The stored-secret store keeps the rule in its strongest form: agents can create, list, inspect metadata and delete secrets, but payload read is not a tool at all — values come back only through the portal/CLI reveal-once flows. If an agent asks you for a private key, that is the system working — say no.

Credential expiry has a ceiling; cross-tenant anything is 404. A credential expiry, when set, is capped at 365 days — and agent-minted credentials always expire, at 7 days or less. Set one, and schedule rotation before it — the two-key overlap pattern from IAM works identically (rotate adds a key; the old one works until revoked). And a debugging note that saves an afternoon: a resource in another tenant returns 404, never 403 — we do not confirm the existence of things that aren't yours. If an id you're sure of 404s, check which tenant your credential resolves to before assuming the resource is gone.

Destructive operations require consent, everywhere. --yes on the CLI (exit code 3 without it — put IG1_YES=1 in migrated pipelines), typed-name modals in the portal, tier-2 keys on the API, write-ahead audit on the agent surface. AWS CLI never prompts; ours does, because blast-radius control is a feature of a multi-tenant cloud, not friction.

5 · What we will not build: an EC2-compatible API

An EC2 DescribeInstances-compatible endpoint is the classic private-cloud trap: the EC2 surface is thousands of actions with per-action IAM semantics, and any partial implementation breaks exactly the tools that would justify it — Terraform's aws provider, boto3 waiters, Packer — because they probe capabilities we would have to stub. Worse, it would have to fake concepts we deliberately lack (IGW attachment, policy JSON, AZs), turning honest 403s into lies. The migration story is mapping, not masquerade: automation is rewritten once against a smaller, saner surface, with the tables on this page.

6 · The week-1 runbook

Each day ends with a verifiable state.

Day 0 — before you arrive. Tenant provisioned and tier chosen against your inventory (mind the managed-database storage ceilings, per instance and on size × replicas — platform limits that no tier raises, published in the API reference — and the volume+snapshot quota pooling). Credentials minted per pipeline: tier 0 for dashboards, tier 1 for deploy CI, tier 2 held by humans; expiry calendar entries created. CI updated: IG1_YES=1, --yes on destructive verbs, IG1_* env vars mapped from AWS_*. Read §4 in full and the flavor table in §2.

Day 1 — network + first instance. Create network/subnet (IPv4 only), security groups from your AWS rules (one CIDR per rule, no -1), and boot one instance on the ratio-corrected flavor. Allocate a floating IP and confirm reachability from inside the fabric or over the VPN — FIPs are not public. Do not look for an IGW; if the workload needs public reachability this quarter, create an edge exposure now and verify its hostname goes active. Checkpoint: SSH from the bastion/VPN works, and the exposure answers over TLS.

Day 2 — images + object data. Golden AMI chain: aws ec2 export-image → S3 → presigned URL → import_image → poll until active (check the image's minimum root-disk requirement against your flavor). Then the §3 recipe and aws s3 sync your buckets; verify with a presigned GET.

Day 3 — databases. Postgres: create via API/Terraform (the version string is validated), restore via pg_dump/pg_restore. Durability is a create-time choice, not a retrofit: ask for replicas (1–3, scaled in place later) and the backup block (daily base backups + continuous WAL archiving, point-in-time recovery) and the platform runs them for you. MySQL workloads: the VM recipe. Kafka: strip TLS/SASL from client configs.

Day 4 — Kubernetes + load balancing + DNS. Create the cluster (control plane + one worker pool), fetch the kubeconfig (ig1 cluster kubeconfig, merge manually), enable deletion protection, and turn on worker autoscaling with the ceiling your tier admits. Before helm-installing anything with type=LoadBalancer: those Services pend forever today — plan NodePort or the edge instead. Build the LB composite for VM tiers (health monitor on by default; internet_facing for the tiers that were internet-facing NLBs — expect pending until the edge syncs, and an internal-CA certificate on the generated .nip.io hostname); TLS terminates on backends or at the edge — start certificate reissue now, ACM certs do not export. Then attach the real domain to the exposure: that is the name that resolves publicly and gets a publicly-trusted certificate, and it is what a cutover should point at. Import your DNS zone (/import, dry-run first), delegate to ns1/ns2.cloud.ig1.com, and read /delegation until it says delegated before you lower any TTL for real. Checkpoint: your own domain answers over TLS from a machine that has never heard of the lab CA.

Day 5 — observability, cost, cutover gate. Create alarms and confirm their state leaves insufficient_data; wire an autoscaling group for the stateless tier and watch one scale event end-to-end in asg.lifecycle. Register webhooks, fire POST /v1/webhooks/{webhook_id}/test, and verify the signature with the HMAC snippet (key = sha256_hex(secret)) — that one line is what most migrations get wrong. Subscriptions survive redeploys; the delivery log at /deliveries does not, so treat it as a debugging tail and keep GET /v1/events as your reconciliation of record. Budgets: amount_cents — multiply by 100, then test a breach end-to-end. Billing dry-run: stop an instance and read the invoice preview — stopped time shows as storage-only (not billed) since 2026-08-30. Cutover gate: isolation confirmed (your resources 404 from a second tenant's key), backup restore tested (not configured — tested), runbook --yes audit complete, rollback DNS TTLs lowered.

Where next. The networking guide is the long version of every network difference on this page; the elasticity guide covers the scaling loop; auth covers tiers and rotation in depth; and the quickstart gets you from zero to a first server in under ten lines.