Setup guide
Everything you must provision before this skill can run โ the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
Provision a read-only IAM principal on the source cloud (AWS by default, with Cost Explorer and IAM billing access activated) plus an API token and object-storage keys on the target cloud (Hetzner Cloud or OVHcloud), and export them as environment variables so the skill can inventory resources, pull egress-grouped billing detail, and stand up the target landing zone with Terraform.
You need to already have
- AWS account administrator (or a delegated IAM admin) able to create an IAM role/user and attach managed policies in the account(s) being inventoried
- Access to the AWS Organizations management account (or a designated billing account) โ Cost Explorer and the ce:GetCostAndUsage API can only be enabled there, and Activate IAM Access must be turned on in the Billing console
- Owner or Admin on the target Hetzner Cloud project (project-scoped API tokens can only be created by a project member) or an OVHcloud account with the Public Cloud project and an Administrator/Compute Operator user
- Local tooling installed: AWS CLI v2, Terraform >= 1.9, rclone >= 1.65, psql/pg_dump matching the source Postgres major version, and optionally hcloud CLI, openstack CLI, gcloud, or az with the resource-graph extension
- Network path from the machine running the skill to both providers' API endpoints on 443 (no egress proxy stripping SigV4 Authorization headers)
- For the GCP source path: a project with the Cloud Asset API enabled; for the Azure source path: at least Reader at the subscription or management-group scope
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
AWS_ACCESS_KEY_ID | AWS access key ID (read-only inventory principal) format: 20-character uppercase alphanumeric string beginning AKIA (long-lived key) or ASIA (STS temporary key) | AWS Management Console > IAM > Users > <your read-only user> > Security credentials > Access keys > Create access key; or, preferred, IAM > Roles > <migration-readonly role> and obtain temporary keys via 'aws sts assume-role --role-arn <arn> --role-session-name migration-audit' |
AWS_SECRET_ACCESS_KEYsecret | AWS secret access key format: 40-character base64-style string (mixed case, digits, + and /) | Shown exactly once at IAM > Users > <user> > Security credentials > Access keys > Create access key > Retrieve access keys; it cannot be re-displayed, only rotated |
AWS_SESSION_TOKENsecretoptional | AWS STS session token format: Long opaque base64 string (typically 300-1000+ characters) | Returned in the Credentials block of 'aws sts assume-role' or 'aws sso login' output; required only when AWS_ACCESS_KEY_ID starts with ASIA |
AWS_REGION | Default AWS region for inventory calls format: Region code, e.g. eu-west-1 or us-east-1 | AWS Management Console > region selector in the top navigation bar; note that Cost Explorer (ce) is a global endpoint served from us-east-1 regardless of this value |
HCLOUD_TOKENsecretoptional | Hetzner Cloud project API token format: 64-character alphanumeric string | Hetzner Cloud Console > select the target project > Security > API tokens > Generate API token; choose Read for inventory/quota checks only, or Read & Write for 'terraform apply'. Shown once. Read directly from the environment by both the hetznercloud/hcloud Terraform provider and the hcloud CLI |
RCLONE_CONFIG_DSTTGT_ACCESS_KEY_IDoptional | Target object-storage access key (rclone remote 'dsttgt') format: S3-compatible access key ID (20-32 characters, provider dependent) | Hetzner: Cloud Console > project > Object Storage > Credentials > Generate credentials. OVHcloud: Manager > Public Cloud > Object Storage > S3 Users > Create user, then download the S3 credentials |
RCLONE_CONFIG_DSTTGT_SECRET_ACCESS_KEYsecretoptional | Target object-storage secret key format: 40+ character opaque secret | Displayed once alongside the access key when the S3 credential pair is created; on OVHcloud it is only available in the downloaded credentials file |
RCLONE_CONFIG_DSTTGT_ENDPOINToptional | Target S3-compatible endpoint URL format: Hostname or https URL, e.g. fsn1.your-objectstorage.com or s3.gra.io.cloud.ovh.net | Hetzner: Cloud Console > Object Storage > bucket > Overview (endpoint is per-location: fsn1/nbg1/hel1). OVHcloud: Manager > Public Cloud > Object Storage > container > shown as the S3 endpoint for the region |
SRC_URLsecretoptional | Source PostgreSQL connection URI (publisher) format: postgresql://user:password@host:5432/dbname?sslmode=require | AWS Console > RDS > Databases > <instance> > Connectivity & security > Endpoint and port; the role used must have REPLICATION (or rds_superuser on RDS) to create a publication |
DST_URLsecretoptional | Target PostgreSQL connection URI (subscriber) format: postgresql://user:password@host:5432/dbname?sslmode=require | OVHcloud Manager > Public Cloud > Databases > <cluster> > Users/Connection info; or the connection string for the self-managed Patroni cluster you provisioned on Hetzner |
OS_CLOUDoptional | OpenStack cloud profile name for the OVHcloud target format: Profile key defined in ~/.config/openstack/clouds.yaml, e.g. ovh-gra | OVHcloud Manager > Public Cloud > project > Users & Roles > create a user with the Compute Operator role, then Download OpenRC file / clouds.yaml; used by 'openstack quota show' and the OpenStack Terraform provider |
Permissions to grant
arn:aws:iam::aws:policy/job-function/ViewOnlyAccessIAM roleadmin consent requiredBlanket read-only across EC2, RDS, ELB, S3 listings and CloudWatch so the inventory step can enumerate every resource class without a bespoke policy per service.
โณ Prefer an inline policy granting only tag:GetResources, ec2:Describe*, rds:DescribeDBInstances, elasticloadbalancing:Describe*, s3:ListAllMyBuckets, s3:GetBucketLocation, cloudwatch:GetMetricStatistics, ce:GetCostAndUsage, savingsplans:DescribeSavingsPlans โ ViewOnlyAccess also exposes unrelated service metadata.
tag:GetResourcesIAM roleadmin consent requiredDrives the mechanical inventory sweep ('aws resourcegroupstaggingapi get-resources') that produces the raw JSON under inventory/ before any per-service describe calls.
โณ Cannot be narrowed by resource ARN โ the Resource Groups Tagging API only supports Resource: "*"; scope it with a Condition on aws:RequestedRegion instead.
ec2:DescribeInstances, ec2:DescribeVolumes, ec2:DescribeReservedInstancesIAM roleadmin consent requiredEnumerates compute flavours for the service-equivalence map, block volume size/type/IOPS for the storage cost model, and existing Reserved Instance commitments that dominate the go/no-go math.
โณ EC2 Describe* actions do not support resource-level permissions; restrict with a Condition on aws:RequestedRegion to the regions actually in scope.
rds:DescribeDBInstancesIAM roleadmin consent requiredCaptures engine, version and instance class per managed database so the plan can price the managed-service premium and flag Aurora-only features with no target equivalent.
โณ Scope Resource to arn:aws:rds:<region>:<account>:db:* if only a subset of accounts is in migration scope.
elasticloadbalancing:DescribeLoadBalancersIAM roleadmin consent requiredMaps ALB/NLB listeners and target groups onto Hetzner lb11/lb21/lb31 or OVHcloud Octavia load balancers and checks per-size target and service caps.
โณ Add elasticloadbalancing:DescribeTargetGroups only if you need per-target detail; otherwise leave it out.
s3:ListAllMyBuckets, s3:GetBucketLocationIAM roleadmin consent requiredDiscovers every bucket and its region so the transfer plan can size data per bucket and confirm the source region for egress pricing.
โณ s3:ListAllMyBuckets is account-wide by definition; if the bucket list is already known, drop it and grant only s3:GetBucketLocation on the specific bucket ARNs.
cloudwatch:GetMetricStatisticsIAM roleadmin consent requiredReads AWS/S3 BucketSizeBytes and NumberOfObjects so data volume comes from metrics rather than from an expensive full object listing.
โณ CloudWatch read actions do not support resource-level scoping; constrain with a Condition on cloudwatch:namespace equal to AWS/S3 and AWS/EC2.
ce:GetCostAndUsageIAM roleadmin consent requiredPulls three months of billing detail grouped by USAGE_TYPE โ the only way to separate internet egress from inter-AZ transfer and NAT processing, which is the core of the egress cost model.
โณ No narrower alternative exists; ce:GetCostAndUsage is already the single most limited Cost Explorer read action. Do not substitute ce:* โ it includes budget and anomaly-monitor mutations.
savingsplans:DescribeSavingsPlansIAM roleadmin consent requiredRetrieves active Savings Plans and their end dates so the cost comparison is against the committed rate, not on-demand list price.
โณ Pair with ce:GetSavingsPlansUtilization only if utilisation history is genuinely needed; the plan inventory alone is usually sufficient.
s3:ListBucket, s3:GetObjectIAM roleadmin consent requiredGrants the rclone 'awssrc' remote the reads it needs to sync and then checksum-verify every object during the bulk migration passes.
โณ Scope Resource to the exact bucket ARNs being migrated (arn:aws:s3:::bucket and arn:aws:s3:::bucket/*) rather than "*"; add s3:GetObjectVersion only if versioned history must move.
roles/cloudasset.viewerIAM roleadmin consent requiredRequired on the GCP source path for 'gcloud asset search-all-resources', which is the GCP equivalent of the tagging-API inventory sweep.
โณ Grant at the project scope being migrated rather than at the organization; roles/viewer also works but is far broader.
ReaderIAM roleadmin consent requiredRequired on the Azure source path so 'az graph query' can project name, type, location and sku for every resource in the subscription.
โณ Assign at the specific subscription or resource-group scope rather than the management group; Resource Graph results are already filtered to what the principal can read.
Hetzner Cloud project API token โ ReadAPI scopeLets the skill list existing servers, volumes, networks and load balancers and confirm project quota headroom before the pilot.
โณ Use a Read token for the discovery and quota-verification phase and only mint a Read & Write token at the moment you run 'terraform apply'; tokens are project-scoped, so create a dedicated migration project rather than reusing production.
Hetzner Cloud project API token โ Read & WriteAPI scopeNeeded for the reference landing zone: creating hcloud_network, hcloud_network_subnet, hcloud_server, hcloud_load_balancer and its network/target/service attachments.
โณ No finer-grained scopes exist in the Hetzner Cloud API โ the only real containment is project isolation plus revoking the token immediately after cutover.
Compute OperatorIAM roleadmin consent requiredOVHcloud Public Cloud user role that permits creating instances, volumes and networks in the target project and reading quotas via 'openstack quota show'.
โณ Use ObjectStore Operator alone for a storage-only migration, and Network Operator alone for the private-network prerequisites; avoid Administrator, which also grants billing and user management.
Step by step
- 1
Create a dedicated read-only IAM principal in the source AWS account
AWS Management Console > IAM > Roles > Create role > Custom trust policy (or Users > Create user for a break-glass key). Name it migration-readonly. On Add permissions attach arn:aws:iam::aws:policy/job-function/ViewOnlyAccess, then Add inline policy with the exact actions tag:GetResources, ce:GetCostAndUsage, savingsplans:DescribeSavingsPlans, ec2:DescribeReservedInstances, cloudwatch:GetMetricStatistics. Do this once per account in scope โ the tagging API and Cost Explorer do not cross account boundaries by themselves.
Open in the console / docs โ - 2
Enable Cost Explorer and turn on IAM access to billing data
Sign in to the Organizations management account. Billing and Cost Management console > Cost Explorer > Launch Cost Explorer (one-time enablement; historical data can take up to 24 hours to populate). Then Billing and Cost Management > Account settings (or IAM user and role access to Billing information) > Edit > tick Activate IAM Access > Update. Without this, ce:GetCostAndUsage returns AccessDeniedException even with the correct IAM policy attached.
Open in the console / docs โ - 3
Export the AWS credentials and verify the caller identity
Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (and AWS_SESSION_TOKEN when using assume-role). Confirm with 'aws sts get-caller-identity' and 'aws sts get-access-key-info --access-key-id $AWS_ACCESS_KEY_ID'. Prefer AWS IAM Identity Center ('aws sso login --profile <p>' plus AWS_PROFILE) over long-lived AKIA keys for anything touching production billing data.
Open in the console / docs โ - 4
If the source is GCP or Azure, enable the equivalent read paths
GCP: Google Cloud console > APIs & Services > Library > search Cloud Asset API > Enable, then IAM & Admin > IAM > Grant access and assign roles/cloudasset.viewer to the principal running gcloud. Azure: Subscriptions > <subscription> > Access control (IAM) > Add role assignment > Reader, then install the CLI extension with 'az extension add --name resource-graph' before running 'az graph query'.
Open in the console / docs โ - 5
Create the target Hetzner Cloud project and API token
Hetzner Cloud Console > Projects > New Project (name it after the migration, not the workload) > open the project > Security > API tokens > Generate API token > choose Read for discovery, Read & Write when you are ready to apply Terraform. Copy the 64-character value immediately โ it is displayed only once. Export it as HCLOUD_TOKEN; both the hcloud CLI and the hetznercloud/hcloud Terraform provider read that variable with no further configuration.
Open in the console / docs โ - 6
Alternatively, provision the OVHcloud target credentials
For the OVHcloud Terraform provider create an application key/secret/consumer key at the OVH token creation page, restricting the granted routes to GET/POST/PUT/DELETE on /cloud/* only. For the OpenStack path, OVHcloud Manager > Public Cloud > project > Users & Roles > Create user with the Compute Operator role > Download the OpenRC/clouds.yaml file, then set OS_CLOUD to the profile name.
Open in the console / docs โ - 7
Create S3-compatible credentials on the target and define the rclone remotes
Hetzner: Cloud Console > project > Object Storage > Credentials > Generate credentials, then note the per-location endpoint (fsn1/nbg1/hel1.your-objectstorage.com). OVHcloud: Manager > Public Cloud > Object Storage > S3 Users > Create user and download the credentials. Define two rclone remotes of type s3 named awssrc and dsttgt โ either via 'rclone config' or by exporting RCLONE_CONFIG_DSTTGT_TYPE=s3 together with the ACCESS_KEY_ID, SECRET_ACCESS_KEY and ENDPOINT variables. Set provider = Other and force_path_style where the target requires it.
Open in the console / docs โ - 8
Check target quotas and file raise requests before the pilot
New projects ship conservative defaults (roughly 10 servers on Hetzner, ~20 instances on an OVHcloud Public Cloud project, with separate caps on volumes, floating IPs and load balancers). Run 'hcloud server list' plus 'hcloud load-balancer list', or 'openstack quota show', and open the increase request days ahead โ discovering a cap mid-cutover forces a rollback. Hetzner raises are requested from Cloud Console > project > Limits (or via support); OVHcloud raises go through the Manager support ticket flow.
Open in the console / docs โ - 9
Prepare the source database for logical replication
On RDS/Aurora PostgreSQL, create a custom DB parameter group with rds.logical_replication = 1, apply it to the instance and reboot (this is a static parameter). Grant rds_superuser (or REPLICATION on self-managed Postgres) to the migration role, and confirm with "psql -d $SRC_URL -Atc 'SHOW wal_level'" returning logical before creating the publication.
Open in the console / docs โ - 10
Initialise Terraform against the target and validate before applying
In the migration/ directory run 'terraform init', 'terraform validate' and 'terraform fmt -check', then 'terraform plan'. The hetznercloud/hcloud provider authenticates purely from HCLOUD_TOKEN โ do not put the token in a .tf file or a committed .tfvars. Keep state in a remote backend with locking, because a half-applied landing zone during a cutover window is a rollback.
Open in the console / docs โ
Check it worked
aws sts get-caller-identity && aws ce get-cost-and-usage --time-period Start=2026-06-01,End=2026-07-01 --granularity MONTHLY --metrics UnblendedCost --group-by Type=DIMENSION,Key=USAGE_TYPE --query 'ResultsByTime[0].Groups[0]' && aws resourcegroupstaggingapi get-resources --region "$AWS_REGION" --resources-per-page 1 --query 'ResourceTagMappingList[0].ResourceARN' && curl -sS -o /dev/null -w 'hcloud=%{http_code}\n' -H "Authorization: Bearer $HCLOUD_TOKEN" https://api.hetzner.cloud/v1/serversIf you hit an error
An error occurred (AccessDeniedException) when calling the GetCostAndUsage operation: User: arn:aws:iam::123456789012:user/migration-readonly is not authorized to perform: ce:GetCostAndUsageCause: Either the ce:GetCostAndUsage action is missing from the policy, or IAM access to Billing information was never activated in the management account โ the IAM policy alone is not sufficient.
Fix: Attach ce:GetCostAndUsage on Resource "*", then in the management account go to Billing and Cost Management > Account settings > IAM user and role access to Billing information > Edit > Activate IAM Access > Update.
An error occurred (DataUnavailableException) when calling the GetCostAndUsage operationCause: Cost Explorer was only just enabled, or the requested time period predates the account's available cost data. Backfill takes up to 24 hours.
Fix: Wait for the initial Cost Explorer preparation to finish, then re-run with a Start date inside the range the console itself displays; shorten the window to the last full month if unsure.
An error occurred (ExpiredToken) when calling the DescribeInstances operation: The security token included in the request is expiredCause: An STS assume-role or IAM Identity Center session expired mid-inventory; ASIA-prefixed keys are short-lived (1-12 hours).
Fix: Re-run 'aws sso login --profile <p>' or 'aws sts assume-role --duration-seconds 3600 ...' and re-export AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN together โ a stale AWS_SESSION_TOKEN left in the environment is the usual cause.
An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.Cause: The principal lacks ec2:DescribeInstances, or a permissions boundary / SCP on the organizational unit denies the action in that region.
Fix: Attach ViewOnlyAccess (or the explicit ec2:Describe* inline policy), then check AWS Organizations > Policies > Service control policies for a Deny with a aws:RequestedRegion condition excluding your AWS_REGION.
An error occurred (AccessDenied) when calling the ListBuckets operation: Access DeniedCause: s3:ListAllMyBuckets is missing, or the bucket policy / S3 Block Public Access configuration denies the cross-account principal.
Fix: Add s3:ListAllMyBuckets on Resource "*" plus s3:ListBucket and s3:GetObject on the specific bucket ARNs; for cross-account inventory also add an allow for the principal in the bucket policy.
SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.Cause: rclone is signing for the wrong region or using virtual-host addressing against an S3-compatible endpoint that expects path style โ very common against Hetzner Object Storage and OVHcloud.
Fix: In the dsttgt remote set provider = Other, region to the endpoint's location (e.g. fsn1 or gra), and force_path_style = true; confirm RCLONE_CONFIG_DSTTGT_ENDPOINT has no trailing slash and no bucket name in the path.
InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records.Cause: The object-storage key pair was generated in a different Hetzner/OVHcloud project or location than the endpoint being addressed โ these credentials are scoped per project and per region.
Fix: Regenerate the S3 credentials inside the same project and location as the target bucket and point RCLONE_CONFIG_DSTTGT_ENDPOINT at that location's endpoint.
Error: unable to authenticate: unauthorized (unauthorized)Cause: HCLOUD_TOKEN is empty, truncated, revoked, or belongs to a different Hetzner Cloud project than the resources in state.
Fix: Regenerate the token at Cloud Console > project > Security > API tokens and re-export it; verify with 'curl -H "Authorization: Bearer $HCLOUD_TOKEN" https://api.hetzner.cloud/v1/servers' returning 200 before re-running terraform.
Error: server limit exceeded (resource_limit_exceeded)Cause: The Hetzner Cloud project's default server, volume, floating-IP or load-balancer quota was hit mid-apply โ new projects start with low limits.
Fix: Request a limit increase for the project before the pilot and re-run 'terraform apply'; partially created resources remain in state, so do not taint or destroy blindly.
ERROR: must be superuser to create FOR ALL TABLES publicationCause: CREATE PUBLICATION mig FOR ALL TABLES requires superuser on PostgreSQL 15 and earlier; the RDS master user is not a true superuser.
Fix: Grant rds_superuser to the migration role on RDS, or on PostgreSQL 16+ grant pg_create_subscription and use an explicit FOR TABLES IN SCHEMA public publication instead of FOR ALL TABLES.
ERROR: logical decoding requires wal_level >= logicalCause: The source instance is still running with wal_level = replica; on RDS this is controlled by the static parameter rds.logical_replication.
Fix: Set rds.logical_replication = 1 in a custom DB parameter group, apply it and reboot the instance (a static parameter change is not dynamic), then re-check with SHOW wal_level.
ERROR: duplicate key value violates unique constraint "orders_pkey"Cause: Sequences were not resynchronised after promotion โ PostgreSQL logical replication copies rows but not sequence values, so the target restarts allocation from 1.
Fix: Immediately after promotion run setval() for every sequence from max(id) on the corresponding column, before the application is allowed to write; generate the statements from pg_sequences rather than by hand.
ERROR: (gcloud.asset.search-all-resources) PERMISSION_DENIED: Request denied by Cloud IAM.Cause: The principal lacks cloudasset.assets.searchAllResources at the requested scope, or the scope string is malformed (must be projects/<id>, folders/<id> or organizations/<id>).
Fix: Grant roles/cloudasset.viewer at the exact scope being searched and confirm the Cloud Asset API is enabled for the project with 'gcloud services enable cloudasset.googleapis.com'.
az: 'graph' is not in the 'az' command group. See 'az --help'.Cause: Azure Resource Graph is a CLI extension and is not installed by default.
Fix: Run 'az extension add --name resource-graph', then re-run the query; if the extension is present but returns no rows, the principal is missing Reader at the subscription scope.
Official documentation: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html โ
API operations this skill uses (20)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Sweep tagged resource inventory Produces the mechanical, account-wide resource listing saved under inventory/ that seeds the service-equivalence map โ step 1 of the procedure explicitly forbids hand-built inventories. | CLI aws resourcegroupstaggingapi get-resources --region <r> --output json (POST / with X-Amz-Target: ResourceGroupsTaggingAPI_20170126.GetResources) | tag:GetResources | docs โ |
| Describe EC2 instances Reads instance type, vCPU/RAM and placement so every instance can be given a named target flavour (ccx33, b3-32, CPX) instead of a size class, and so shared vs dedicated vCPU swaps are flagged. | CLI aws ec2 describe-instances --region <r> (Action=DescribeInstances) | ec2:DescribeInstances | docs โ |
| Describe EBS volumes Supplies block-storage GB, volume type and provisioned IOPS for the storage cost model and for the gp3-to-network-attached-volume gap that must be benchmarked with fio. | CLI aws ec2 describe-volumes --query 'Volumes[].{id:VolumeId,gb:Size,type:VolumeType,iops:Iops}' | ec2:DescribeVolumes | docs โ |
| Describe RDS database instances Captures engine, version, instance class and storage per database to price the 1.6-2.5x managed-service premium and to identify Aurora fast clones / Global Database, which have no target equivalent. | CLI aws rds describe-db-instances --region <r> | rds:DescribeDBInstances | docs โ |
| Describe application and network load balancers Maps each ALB/NLB onto a Hetzner lb11/lb21/lb31 or an OVHcloud Octavia load balancer and checks the per-size target and service caps recorded in the equivalence table. | CLI aws elbv2 describe-load-balancers --region <r> | elasticloadbalancing:DescribeLoadBalancers | docs โ |
| List S3 buckets Enumerates the object-storage estate that has to be synced, feeding both the per-bucket transfer plan and the one-time migration egress calculation. | CLI aws s3api list-buckets (GET / on the S3 service endpoint) | s3:ListAllMyBuckets | docs โ |
| Read bucket size from CloudWatch metrics Sizes each bucket from daily storage metrics rather than by listing objects, which is what makes the days = TB / (Gbps x 10.8 x utilisation) transfer estimate cheap to compute. | CLI aws cloudwatch get-metric-statistics --namespace AWS/S3 --metric-name BucketSizeBytes --dimensions Name=BucketName,Value=<b> Name=StorageType,Value=StandardStorage --period 86400 --statistics Average | cloudwatch:GetMetricStatistics | docs โ |
| Pull cost and usage grouped by usage type The central cost call โ USAGE_TYPE grouping is the only way to separate internet egress from inter-AZ transfer, NAT processing and ALB LCUs, all of which hide inside the 'EC2 - Other' service line. | CLI aws ce get-cost-and-usage --time-period Start=<d>,End=<d> --granularity MONTHLY --metrics UnblendedCost UsageQuantity --group-by Type=DIMENSION,Key=USAGE_TYPE | ce:GetCostAndUsage | docs โ |
| Describe active Savings Plans Retrieves commitment type, hourly commitment and end date so the target comparison is against the committed rate โ comparing list-to-list inflates savings 2-3x and is the top listed pitfall. | CLI aws savingsplans describe-savings-plans --states active | savingsplans:DescribeSavingsPlans | docs โ |
| Describe Reserved Instances Establishes the unrecoverable RI residual that must be netted into the one-time cost table and the payback_months calculation. | CLI aws ec2 describe-reserved-instances --filters Name=state,Values=active | ec2:DescribeReservedInstances | docs โ |
| Search all GCP resources (Cloud Asset Inventory) The GCP-source equivalent of the tagging-API sweep, producing the same inventory/ JSON shape when the migration origin is Google Cloud rather than AWS. | CLI gcloud asset search-all-resources --scope=projects/<p> --format=json (v1 searchAllResources) | cloudasset.assets.searchAllResources (roles/cloudasset.viewer) | docs โ |
| Query Azure resources via Resource Graph The Azure-source inventory path; projects name, type, location and sku so Azure VM sizes and disks can be mapped onto the target flavours. | CLI az graph query -q "Resources | project name,type,location,sku" (POST /providers/Microsoft.ResourceGraph/resources) | Reader at subscription or management-group scope | docs โ |
| List Hetzner Cloud servers / confirm quota headroom Verifies the target project's current server count against its limit before the pilot, per the step that warns a cap discovered mid-cutover guarantees a rollback. | GET GET https://api.hetzner.cloud/v1/servers (hcloud server list) | Hetzner Cloud project API token โ Read | docs โ |
| Create the target private network Builds the 10.42.0.0/16 landing-zone network and the eu-central cloud subnet that the app tier and load balancer attach to in the reference Terraform. | POST POST https://api.hetzner.cloud/v1/networks (Terraform hcloud_network / hcloud_network_subnet) | Hetzner Cloud project API token โ Read & Write | docs โ |
| Create target compute instances Provisions the dedicated-vCPU app tier of the landing zone that EC2 instances are migrated onto. | POST POST https://api.hetzner.cloud/v1/servers (Terraform hcloud_server, server_type ccx33) | Hetzner Cloud project API token โ Read & Write | docs โ |
| Create the target load balancer and attach private targets Creates the lb11 edge with the http service, /healthz health check and use_private_ip targets that replace the source ALB at cutover. | POST POST https://api.hetzner.cloud/v1/load_balancers (plus /v1/load_balancers/{id}/actions/attach_to_network and /add_target) | Hetzner Cloud project API token โ Read & Write | docs โ |
| Show OpenStack project quotas on OVHcloud Confirms instance, volume, floating-IP and load-balancer limits on the OVHcloud target so raise requests are filed days before the pilot rather than during the cutover window. | CLI openstack quota show (GET /v2.1/os-quota-sets/{tenant_id}) | Compute Operator role on the OVHcloud Public Cloud project | docs โ |
| Bulk-sync a bucket to the target endpoint Executes the full sync followed by repeated delta passes until one pass fits inside the freeze window, which is the object-storage half of the stateful cutover plan. | CLI rclone sync awssrc:<bucket> dsttgt:<bucket> --fast-list --checksum --transfers 64 --s3-chunk-size 64M (S3 ListObjectsV2 + GetObject on source, CreateMultipartUpload/UploadPart on target) | s3:ListBucket and s3:GetObject on the source bucket ARNs; write credentials on the target endpoint | docs โ |
| Verify object parity before cutover The gate that aborts the cutover when any object differs โ the quality checklist requires every stateful store to have a verification command that fails loudly. | CLI rclone check awssrc:<bucket> dsttgt:<bucket> --one-way --combined /tmp/<bucket>-check.txt (HeadObject / checksum comparison) | s3:ListBucket and s3:GetObject on the source bucket ARNs | docs โ |
| Snapshot Kubernetes state before cutover Captures namespace manifests for restore on the target cluster; PVCs are deliberately rebuilt from the storage sync rather than from this snapshot unless the CSI drivers match. | CLI velero backup create pre-cutover --include-namespaces prod --wait | Cluster-admin on the source cluster plus write access to the Velero object-storage bucket | docs โ |