Use case: AWS Organization A / Account A → AWS Organization B / Account B
Objective: Retain the exact same public IPv4 address while changing AWS account ownership.
1. Executive Summary
Final answer
Yes. AWS supports transferring a normal Amazon-provided Elastic IP address between AWS accounts that belong to completely different AWS Organizations.
The accounts do not need to belong to the same AWS Organization. AWS explicitly states that an Elastic IP can be transferred from any AWS account to any other AWS account in the same AWS Region. AWS also explicitly discusses an EIP being transferred to an account outside the Organization, confirming cross-Organization transfers are supported.
For this migration:
AWS Organization A
└── Source Account A
└── EIP 1.2.3.4
↓ ownership transfer
AWS Organization B
└── Destination Account B
└── SAME public EIP 1.2.3.4
The public IPv4 address remains the same.
However:
- the EIP’s AWS allocation ID changes after successful transfer;
- tags do not transfer;
- EC2/ENI/NAT associations do not transfer;
- VPCs do not transfer;
- NAT gateways do not transfer;
- DNS resources do not transfer;
- PTR/reverse-DNS configuration must be removed before acceptance;
- the source and destination must use the same AWS Region.
AWS’s Knowledge Center specifically shows the destination receiving a new AllocationId after successful transfer.
Important distinction
| Operation | What it means |
|---|---|
| Transfer EIP | Changes which AWS account owns the public IPv4 address |
| Associate EIP | Connects an EIP to an EC2 instance or ENI |
| NAT Gateway EIP | NAT gateway consumes an EIP; NAT gateway itself does not transfer |
| Move EC2 | Separate migration operation |
| Move NAT Gateway | Not supported; recreate it |
| Move VPC | Not part of EIP transfer; recreate/migrate networking independently |
2. Supported and Unsupported Scenarios
| Scenario | Supported? | Notes |
|---|---|---|
| Same AWS account | N/A | No ownership transfer needed; just associate/disassociate |
| Different accounts, same Organization | ✅ | Supported |
| Different accounts, different Organizations | ✅ | Explicitly supported |
| Different Regions | ❌ | Source and destination must use same Region |
| Standard Amazon-provided EC2 EIP | ✅ | Normal supported case |
| BYOIP EIP | ❌ | AWS explicitly excludes BYOIP addresses |
| Amazon-provided contiguous EIP from IPAM pool | ❌ | Use IPAM sharing instead |
| Customer-owned IP / Outposts CoIP | ❌ | Cannot use EIP transfer |
| EIP associated with EC2 | ⚠️ | Transfer can be enabled, but acceptance fails until disassociated |
| EIP associated with ENI | ⚠️ | Same restriction |
| Primary EIP on NAT Gateway | ⚠️ | Must be freed before transfer completion; normally delete NAT gateway |
| Secondary EIP on NAT Gateway | ⚠️ | Can be disassociated using NAT gateway address APIs |
| Reverse DNS/PTR configured | ⚠️ | Transfer can start, but destination cannot accept until PTR is removed |
AWS documents the same-Region restriction, BYOIP/IPAM/CoIP exclusions, tags reset, reverse-DNS restriction, and cross-Organization behavior explicitly.
3. Requirements and Prerequisites
Record these before starting:
Source Account ID
Destination Account ID
AWS Region
Public EIP
Source Allocation ID
Association ID
Network Interface ID
Instance ID
NAT Gateway ID
Subnet ID
Availability Zone
Network Border Group
Route tables
EIP tags
PTR record
External allowlists
Terraform resource addresses/state
Use:
export AWS_REGION=ap-northeast-1
export SOURCE_PROFILE=evp-staging-old
export DEST_PROFILE=evp-staging-new
export SOURCE_ACCOUNT_ID=111111111111
export DEST_ACCOUNT_ID=222222222222
export EIP=1.2.3.4
export SOURCE_ALLOCATION_ID=eipalloc-xxxxxxxxxxxxxxxxx
AWS CLI
Use AWS CLI v2.
AWS’s EIP-transfer documentation does not state a specific minimum CLI-v2 version. Current CLI v2 contains all required commands:
enable-address-transfer
disable-address-transfer
accept-address-transfer
describe-address-transfers
AWS CLI v1 entered maintenance mode in August 2026, so new production automation should use CLI v2.
Verify:
aws --version
aws ec2 enable-address-transfer help
aws ec2 accept-address-transfer help
4. IAM Permissions
The exact EC2 IAM actions exist in AWS’s current Service Authorization Reference.
Core source-account permissions
ec2:EnableAddressTransfer
ec2:DisableAddressTransfer
ec2:DescribeAddressTransfers
ec2:DescribeAddresses
ec2:DescribeAddressesAttribute
Depending on the attachment:
ec2:DisassociateAddress
ec2:ResetAddressAttribute
ec2:DescribeInstances
ec2:DescribeNetworkInterfaces
ec2:DescribeNatGateways
ec2:DeleteNatGateway
ec2:DescribeRouteTables
ec2:DescribeSubnets
ec2:DisassociateAddress is the documented action for EC2/ENI EIP disassociation.
Core destination-account permissions
ec2:AcceptAddressTransfer
ec2:DescribeAddresses
ec2:DescribeAddressTransfers
ec2:CreateTags
ec2:AssociateAddress
NAT migration can additionally require:
ec2:CreateNatGateway
ec2:DescribeNatGateways
ec2:CreateRoute
ec2:ReplaceRoute
ec2:DeleteRoute
Reverse DNS recreation can require:
ec2:ModifyAddressAttribute
ec2:DescribeAddressesAttribute
AWS confirms AcceptAddressTransfer, AssociateAddress, EnableAddressTransfer, DisableAddressTransfer, ResetAddressAttribute, and the related Describe actions in its authorization reference.
SCPs and permission boundaries
AWS Organizations itself is not required to perform the transfer.
However, an SCP such as:
{
"Effect": "Deny",
"Action": "ec2:*",
"Resource": "*"
}
could block the migration even when the IAM role itself has AdministratorAccess.
Permissions boundaries, SCPs, session policies, and IAM policies are evaluated together; an explicit deny wins.
5. Destination EIP Quota
Default AWS VPC quota:
Elastic IP addresses per Region = 5
It is adjustable.
The quota code is:
L-0263D0A3
Check destination quota:
aws service-quotas get-service-quota \
--service-code ec2 \
--quota-code L-0263D0A3 \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Count destination EIPs:
aws ec2 describe-addresses \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'length(Addresses)'
If quota is exhausted, acceptance can fail with:
AddressLimitExceeded
AWS explicitly documents this failure mode.
6. Pre-Migration Discovery
Confirm source identity
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
Expected account:
111111111111
Do not continue if it differs.
Discover the EIP
aws ec2 describe-addresses \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
For a specific EIP:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Useful filtered output:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
--query 'Addresses[0].{
PublicIp:PublicIp,
AllocationId:AllocationId,
AssociationId:AssociationId,
InstanceId:InstanceId,
NetworkInterfaceId:NetworkInterfaceId,
PrivateIpAddress:PrivateIpAddress,
NetworkBorderGroup:NetworkBorderGroup,
PublicIpv4Pool:PublicIpv4Pool,
Tags:Tags
}'
Check:
PublicIpv4Pool = amazon
for a conventional Amazon-provided EIP.
7. Determine Whether EIP Belongs to a NAT Gateway
aws ec2 describe-nat-gateways \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
--query "NatGateways[?NatGatewayAddresses[?PublicIp=='${EIP}']].{
NatGatewayId:NatGatewayId,
State:State,
SubnetId:SubnetId,
VpcId:VpcId,
Addresses:NatGatewayAddresses
}"
If you get a NAT gateway ID:
export SOURCE_NAT_GW_ID=nat-xxxxxxxxxxxxxxxxx
Inspect it:
aws ec2 describe-nat-gateways \
--nat-gateway-ids "$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Find routes using it:
aws ec2 describe-route-tables \
--filters "Name=route.nat-gateway-id,Values=$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
8. Why a NAT Gateway Does Not Transfer
An EIP transfer changes ownership of the IP address only.
The following remain in the source account:
VPC
Subnet
Internet Gateway
Route Table
NAT Gateway
NAT Gateway ENI
Security configuration
For a conventional zonal NAT gateway, the primary EIP cannot simply be detached and moved while keeping the gateway alive.
AWS provides DisassociateNatGatewayAddress only for secondary EIPs. AWS explicitly states:
You cannot disassociate your primary EIP.
Deleting the NAT gateway:
- disassociates the EIP;
- does not release the EIP;
- leaves routes pointing to the deleted NAT gateway in
blackholestate until changed.
This is the crucial behavior for the migration.
9. Pre-Transfer Evidence / Backup
Create a migration directory:
mkdir -p eip-transfer-backup
cd eip-transfer-backup
Save identity:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE" \
> source-account.json
Save EIP:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> source-eip.json
Save tags separately:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
--query 'Addresses[0].Tags' \
> eip-tags.json
Inspect PTR:
aws ec2 describe-addresses-attribute \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--attribute domain-name \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> reverse-dns.json 2>&1 || true
Save NAT gateway:
aws ec2 describe-nat-gateways \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> nat-gateways.json
Save route tables:
aws ec2 describe-route-tables \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> route-tables.json
Save ENIs:
aws ec2 describe-network-interfaces \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> network-interfaces.json
Document outside AWS:
MongoDB Atlas IP allowlists
Confluent Cloud IP allowlists
Partner/vendor firewalls
Third-party SaaS allowlists
Office/on-prem firewalls
External DNS records
Monitoring dependencies
Webhook/firewall restrictions
Because the public IP remains identical, existing IP allowlists normally do not need to change, but they absolutely should be tested after cutover.
10. Reverse DNS / PTR Check
Check:
aws ec2 describe-addresses-attribute \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--attribute domain-name \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Example:
{
"Addresses": [
{
"PublicIp": "1.2.3.4",
"AllocationId": "eipalloc-...",
"PtrRecord": "mail.example.com."
}
]
}
If a PTR exists, AWS allows transfer initiation, but destination acceptance fails until the PTR record is removed.
Remove it:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 reset-address-attribute \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--attribute domain-name \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
AWS documents reset-address-attribute for PTR removal.
After the migration, recreate it with the new destination allocation ID:
aws ec2 modify-address-attribute \
--allocation-id "$DEST_ALLOCATION_ID" \
--domain-name mail.example.com \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
11. AWS Console — Source Account
Use the source account.
Navigate:
AWS Console
→ EC2
→ Network & Security
→ Elastic IP addresses
Then:
Select EIP
→ Actions
→ Enable transfer
AWS currently calls the operation Enable transfer. UI wording can evolve slightly.
For multiple EIPs choose:
Single account
or:
Multiple accounts
Enter:
Destination AWS Account ID
Example:
222222222222
AWS requires confirmation:
enable
Then choose:
Submit
The source account should show:
Transfer status = Pending
AWS does not notify the destination account automatically.
12. AWS CLI — Source Account
First verify identity:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
Then:
aws ec2 enable-address-transfer \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--transfer-account-id "$DEST_ACCOUNT_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
This exact command/API is documented by AWS.
Example result:
{
"AddressTransfer": {
"PublicIp": "1.2.3.4",
"AllocationId": "eipalloc-source...",
"TransferAccountId": "222222222222",
"TransferOfferExpirationTimestamp": "...",
"AddressTransferStatus": "pending"
}
}
Verify:
aws ec2 describe-address-transfers \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Look for:
AddressTransferStatus = pending
TransferAccountId = destination account
PublicIp = expected EIP
13. Transfer Expiration
The authoritative EC2 User Guide and EC2 API Reference state:
Destination has 7 days to accept the transfer.
After seven days, the transfer expires and ownership remains/returns to the source account. Accepted transfers remain visible to the source for 14 days.
AWS documentation inconsistency
The currently generated AWS CLI reference contains a field description saying “seven hours” in some TransferOfferExpirationTimestamp output documentation.
This conflicts with:
- EC2 User Guide;
- VPC User Guide;
- EC2
DescribeAddressTransfersAPI Reference;
which consistently state seven days.
For operational planning, use the EC2 service documentation value:
7 days.
Also inspect the actual:
TransferOfferExpirationTimestamp
returned by AWS for your transfer.
14. Destination Validation Before Acceptance
Confirm destination identity:
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
It must show:
Account = 222222222222
Confirm current Region:
aws configure get region \
--profile "$DEST_PROFILE"
Still pass the Region explicitly anyway:
--region ap-northeast-1
Check target VPC:
aws ec2 describe-vpcs \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Check target public subnet:
aws ec2 describe-subnets \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Check Internet Gateway:
aws ec2 describe-internet-gateways \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Check EIP quota before acceptance.
15. What Happens to the Allocation ID?
This point is critical.
Suppose source has:
Public IP: 1.2.3.4
Allocation ID: eipalloc-OLD111111
After successful transfer:
Public IP: 1.2.3.4
Allocation ID: eipalloc-NEW222222
AWS’s own Knowledge Center explicitly states:
A successful transfer generates a new AllocationId in the destination owner’s account.
Therefore:
Public IPv4 = SAME
Allocation ID = CHANGES
Do not configure destination Terraform with the old allocation ID.
Do not assume the AllocationId shown in the immediate accept-address-transfer response is the destination resource ID.
After acceptance always run:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
and obtain the authoritative destination allocation ID.
16. AWS Console — Destination Account
Sign into the destination account.
Navigate:
AWS Console
→ EC2
→ Network & Security
→ Elastic IP addresses
→ Actions
→ Accept transfer
AWS does not automatically expose a normal “pending EIP” resource in the destination EIP inventory beforehand.
Instead, enter the exact public IPv4 address:
1.2.3.4
Then choose:
Submit
AWS’s console workflow explicitly requires entering the transferred EIP.
After successful acceptance:
Destination owns EIP
Public IP remains identical
EIP has new Allocation ID
Address is initially unassociated
Source tags are absent
17. AWS CLI — Destination Acceptance
Before ownership-changing operation:
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
Accept:
aws ec2 accept-address-transfer \
--address "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
The CLI syntax is current and documented.
Immediately discover destination allocation:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Capture it:
export DEST_ALLOCATION_ID="$(
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'Addresses[0].AllocationId' \
--output text
)"
Print:
echo "$DEST_ALLOCATION_ID"
Verify:
aws ec2 describe-addresses \
--allocation-ids "$DEST_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'Addresses[0].{
PublicIp:PublicIp,
AllocationId:AllocationId,
AssociationId:AssociationId,
NetworkBorderGroup:NetworkBorderGroup,
Tags:Tags
}'
18. Tags
AWS explicitly states:
EIP tags do not transfer.
They are reset when ownership transfer completes.
Before migration:
aws ec2 describe-addresses \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
--query 'Addresses[0].Tags' \
> eip-tags.json
After acceptance:
aws ec2 create-tags \
--resources "$DEST_ALLOCATION_ID" \
--tags file://eip-tags.json \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Verify:
aws ec2 describe-addresses \
--allocation-ids "$DEST_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'Addresses[0].Tags'
19. EC2 Instance Migration Scenario
Source:
EC2-A
└── EIP X
Destination:
EC2-B
└── SAME EIP X
Identify association:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Capture:
export ASSOCIATION_ID=eipassoc-xxxxxxxx
Disassociate:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 disassociate-address \
--association-id "$ASSOCIATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
AWS requires the EIP to be disassociated before destination acceptance; otherwise acceptance can fail with:
InvalidTransfer.AddressAssociated
Accept from destination.
Then:
aws ec2 associate-address \
--allocation-id "$DEST_ALLOCATION_ID" \
--instance-id i-xxxxxxxxxxxxxxxxx \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
20. ENI Migration Scenario
Source:
ENI-A
└── EIP X
Disassociate:
aws ec2 disassociate-address \
--association-id "$ASSOCIATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
After destination accepts:
aws ec2 associate-address \
--allocation-id "$DEST_ALLOCATION_ID" \
--network-interface-id eni-xxxxxxxxxxxxxxxxx \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
If targeting a specific secondary private IP:
aws ec2 associate-address \
--allocation-id "$DEST_ALLOCATION_ID" \
--network-interface-id eni-xxxxxxxxxxxxxxxxx \
--private-ip-address 10.20.1.100 \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
An EIP cannot be associated with an interface whose network border group is incompatible with the EIP.
21. NAT Gateway Migration — Recommended Production Strategy
This is the most important scenario.
Before
Organization A
└── Source AWS Account
└── VPC-A
├── Public Subnet
│ └── NAT Gateway A
│ └── EIP X = 1.2.3.4
│
└── Private Subnets
└── EKS / EC2 workloads
After
Organization B
└── Destination AWS Account
└── VPC-B
├── Public Subnet
│ └── NAT Gateway B
│ └── SAME EIP X = 1.2.3.4
│
└── Private Subnets
└── EKS / EC2 workloads
Can EIP X move together with NAT Gateway A?
No.
NAT Gateway A remains in source account.
Can primary EIP be manually disassociated?
No.
AWS allows explicit disassociation only for secondary NAT gateway EIPs; primary EIP cannot be removed that way.
What frees the primary EIP?
Delete the source NAT gateway.
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 delete-nat-gateway \
--nat-gateway-id "$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
AWS guarantees that deleting the NAT gateway:
disassociates its EIP
does NOT release the EIP
This is exactly what we need.
Never run
aws ec2 release-address ...
during this migration.
That is not part of ownership transfer.
22. NAT Gateway Minimum-Downtime Sequence
Phase A — Days/hours before maintenance
Fully prepare destination:
VPC
CIDRs
public/private subnets
Internet Gateway
route tables
NACLs
EKS
security
applications
Terraform
vendor testing
monitoring
Optionally create a temporary NAT gateway using a temporary EIP to validate destination connectivity.
This allows destination workloads to be tested before final cutover.
Important limitation
A temporary EIP used as the primary EIP of a zonal NAT gateway cannot later simply be removed and replaced by the transferred EIP.
Therefore if the final NAT must use only the transferred EIP:
temporary NAT
→ validate
→ delete temporary NAT
→ create final NAT with transferred EIP
Alternatively, a transferred EIP could be added as a secondary EIP, but the temporary primary remains. That is usually unsuitable when vendors require one deterministic egress IP.
23. Can Routes Be Prepared?
Yes.
You can pre-create:
Route tables
Associations
Subnet mappings
Internet Gateway
Destination subnet layout
However, the actual route:
0.0.0.0/0 → final NAT Gateway
cannot point to a NAT gateway that does not yet exist.
If using a temporary NAT:
Before cutover:
0.0.0.0/0 → temporary NAT
After final NAT available:
0.0.0.0/0 → final NAT
Use:
aws ec2 replace-route \
--route-table-id rtb-xxxxxxxx \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id "$DEST_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
24. Strict NAT Cutover Runbook
Phase 1 — Preparation
Confirm:
[ ] Source account
[ ] Destination account
[ ] ap-northeast-1
[ ] EIP
[ ] Allocation ID
[ ] NAT Gateway
[ ] Routes
[ ] tags
[ ] PTR
[ ] external allowlists
[ ] destination quota
[ ] target subnet
[ ] target Internet Gateway
[ ] Terraform
Phase 2 — Pre-enable transfer
The source can initiate the transfer handshake before final cutover.
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 enable-address-transfer \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--transfer-account-id "$DEST_ACCOUNT_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Verify:
aws ec2 describe-address-transfers \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Do not accept yet.
Phase 3 — Freeze
Stop infrastructure changes involving:
EIP
NAT
route tables
subnets
Terraform
VPC
Pause automatic Terraform applies.
Record final rollback point.
Phase 4 — Reduce Traffic
Where possible:
stop jobs
stop batch traffic
drain application traffic
pause heavy outbound connections
wait for Kafka/Mongo long-lived connections to reduce
Existing TCP connections will not survive the NAT replacement.
Phase 5 — Delete Source NAT
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 delete-nat-gateway \
--nat-gateway-id "$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Monitor:
aws ec2 describe-nat-gateways \
--nat-gateway-ids "$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
--query 'NatGateways[0].State' \
--output text
Verify EIP is no longer associated:
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
You want no active association.
25. Accept the Transfer
Destination identity:
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
Accept:
aws ec2 accept-address-transfer \
--address "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Discover new allocation ID:
export DEST_ALLOCATION_ID="$(
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'Addresses[0].AllocationId' \
--output text
)"
Verify:
echo "Transferred EIP: $EIP"
echo "Destination allocation ID: $DEST_ALLOCATION_ID"
26. Create Destination NAT Gateway
Assume:
export DEST_PUBLIC_SUBNET_ID=subnet-xxxxxxxxxxxxxxxxx
Confirm destination:
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
Create:
aws ec2 create-nat-gateway \
--subnet-id "$DEST_PUBLIC_SUBNET_ID" \
--allocation-id "$DEST_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Capture the returned:
NatGatewayId
Example:
export DEST_NAT_GW_ID=nat-xxxxxxxxxxxxxxxxx
Wait until:
State = available
Check:
aws ec2 describe-nat-gateways \
--nat-gateway-ids "$DEST_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE" \
--query 'NatGateways[0].{
State:State,
NatGatewayId:NatGatewayId,
Addresses:NatGatewayAddresses
}'
The EIP network border group must match the NAT gateway’s AZ/network border group.
27. Update Destination Routes
Example:
export DEST_PRIVATE_ROUTE_TABLE_ID=rtb-xxxxxxxxxxxxxxxxx
Update:
aws ec2 replace-route \
--route-table-id "$DEST_PRIVATE_ROUTE_TABLE_ID" \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id "$DEST_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Verify:
aws ec2 describe-route-tables \
--route-table-ids "$DEST_PRIVATE_ROUTE_TABLE_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Confirm:
0.0.0.0/0
→
new NAT Gateway
28. Validation
From EC2
curl -s https://checkip.amazonaws.com
Expected:
1.2.3.4
From EKS
kubectl run eip-test \
--image=curlimages/curl \
--rm -it \
--restart=Never \
-- https://checkip.amazonaws.com
Expected:
1.2.3.4
The kubectl run syntax is appropriate for creating a temporary one-shot curl pod.
For non-interactive CI-style validation:
kubectl run eip-test \
--image=curlimages/curl \
--restart=Never \
--command -- \
curl -s https://checkip.amazonaws.com
Then:
kubectl logs eip-test
kubectl delete pod eip-test
From multiple EKS namespaces
kubectl run eip-test \
-n YOUR_NAMESPACE \
--image=curlimages/curl \
--rm -it \
--restart=Never \
-- https://checkip.amazonaws.com
29. Vendor Validation
Test at minimum:
MongoDB Atlas
Confluent Cloud
external REST APIs
vendor allowlists
webhooks
container registries
GitHub
AWS APIs
Datadog
Sentry
third-party SaaS endpoints
Because the outbound IP remains the same, IP-based allowlists should continue to match.
However, new TCP/TLS sessions will be created after NAT replacement.
30. DNS Behavior
A normal Route 53 or external DNS record does not transfer automatically with an EIP.
Example:
api.example.com → 1.2.3.4
If it already points to 1.2.3.4, no DNS-value change is required because the public EIP itself remains unchanged.
But the hosted zone and DNS record still belong wherever they were originally managed.
This migration:
transfers EIP ownership
It does not:
transfer Route 53 hosted zones
transfer Route 53 records
transfer DNS provider ownership
PTR/reverse DNS is the special case and must be handled separately as described earlier.
31. Terraform / IaC — Critical Rules
This deserves careful handling.
Suppose source Terraform contains:
resource "aws_eip" "nat" {
domain = "vpc"
lifecycle {
prevent_destroy = true
}
}
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
}
Do not simply delete aws_eip.nat from Terraform and run apply blindly.
The source state currently represents ownership of the EIP.
A safe migration separates:
AWS ownership operations
Terraform state ownership
Terraform configuration
HashiCorp documents that terraform state rm removes the Terraform binding while leaving the remote object intact.
32. Terraform Recommended Migration Sequence
A. Back up state
terraform state pull > source-state-before-eip-transfer.json
List resources:
terraform state list
Identify:
aws_eip.nat
aws_nat_gateway.this
Use the real module paths if applicable:
module.vpc.aws_eip.nat[0]
module.vpc.aws_nat_gateway.this[0]
B. Handle source NAT intentionally
The NAT gateway must actually be deleted as part of the operational migration.
The EIP itself must not be released.
If lifecycle contains:
prevent_destroy = true
do not casually remove it merely to satisfy Terraform.
Separate NAT deletion from EIP state transition.
C. After EIP ownership moves
The source Terraform state must no longer claim ownership of the transferred EIP.
For example:
terraform state rm 'aws_eip.nat'
or:
terraform state rm 'module.vpc.aws_eip.nat[0]'
This removes Terraform’s binding without attempting to destroy the remote resource.
Remove/update the corresponding source configuration so a subsequent plan does not attempt to recreate an EIP.
33. Destination Terraform Import
Create the destination configuration first:
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "evp-staging-nat"
}
}
Then import using the new destination allocation ID:
terraform import \
aws_eip.nat \
"$DEST_ALLOCATION_ID"
Current HashiCorp AWS Provider documentation explicitly imports aws_eip resources using their allocation ID.
Modern Terraform can also use:
import {
to = aws_eip.nat
id = "eipalloc-NEW..."
}
HashiCorp recommends one remote object be bound to one Terraform resource address.
After import:
terraform plan
Expected:
no EIP replacement
no EIP release
no unexpected allocation
Investigate any EIP destroy/replace action before applying.
34. Destination NAT Terraform
Then configure:
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
depends_on = [
aws_internet_gateway.this
]
tags = {
Name = "evp-staging-nat"
}
}
Current AWS provider documentation warns against trying to attach NAT gateway EIPs through the generic EIP network_interface argument; NAT association should be managed by the NAT resource.
35. Rollback
Rollback depends on where you are.
Stage A — Transfer not initiated
Nothing to reverse.
Stage B — Transfer pending, destination has not accepted
Source still controls the transfer.
Check identity:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
Cancel:
aws ec2 disable-address-transfer \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
AWS explicitly supports disabling a transfer whose status is pending.
If old NAT still exists, service continues normally.
Stage C — Old NAT deleted, but EIP not accepted
Disable transfer:
aws ec2 disable-address-transfer \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Then recreate source NAT using the EIP:
aws ec2 create-nat-gateway \
--subnet-id "$SOURCE_PUBLIC_SUBNET_ID" \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Update source routes.
Stage D — Destination has accepted
disable-address-transfer can no longer restore source ownership.
The destination now owns the public IP.
To return it:
Destination account
→ frees EIP
→ EnableAddressTransfer back to original account
→ Original account accepts
Because AWS supports transfer between any two accounts in the same Region, the process can be reversed.
Destination:
aws ec2 enable-address-transfer \
--allocation-id "$DEST_ALLOCATION_ID" \
--transfer-account-id "$SOURCE_ACCOUNT_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
Original source then accepts:
aws ec2 accept-address-transfer \
--address "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
The source will again receive a new allocation ID.
Stage E — Destination NAT already created
Because the EIP is the primary EIP of the destination NAT gateway, first:
delete destination NAT
wait for EIP to become free
reverse-transfer EIP
accept in source
obtain new allocation ID
recreate source NAT
restore source routes
Rollback therefore causes another interruption.
36. Downtime Analysis
AWS-documented facts
AWS does not publish an exact duration guarantee for:
AcceptAddressTransfer completion
NAT gateway deletion
NAT gateway creation
route convergence
Do not design the runbook around an assumed AWS SLA for these steps.
AWS does document that deleting a NAT gateway disassociates the EIP and leaves routes targeting it in blackhole state.
AWS also warns in related NAT migrations that changing NAT routing or disassociating the EIP drops existing connections and they must reconnect.
Operational expectation
Typically the following are quick:
enable transfer
accept transfer
describe-addresses
route-table API update
NAT gateway deletion/creation tends to be the largest component and should be measured during staging rather than assigned an invented production duration.
Existing connections
Expect:
TCP sessions reset/lost
Kafka connections reconnect
MongoDB connections reconnect
HTTPS connections reconnect
HTTP keepalive connections rebuild
DNS unaffected if same EIP
37. Can This Be Zero Downtime?
For the strict requirement:
Source NAT
→ SAME sole EIP
→ Destination NAT
true zero downtime should not be promised.
There is a point where:
old NAT must stop owning EIP
↓
EIP ownership changes
↓
new NAT is created
The exact same EIP cannot simultaneously be the active primary EIP of both NAT gateways in two accounts.
Therefore classify the design as:
Minimum downtime
not:
Zero downtime
Higher-availability alternative
Pre-run destination through:
temporary EIP
+
temporary NAT
and temporarily allowlist both:
OLD/STABLE EIP
TEMPORARY EIP
This allows destination workloads to be validated and activated independently.
At final transfer, switch to the permanent EIP.
This can reduce application migration risk, although the final same-EIP NAT transition still resets active connections.
38. Security and Governance
Use:
separate AWS CLI profiles
explicit --profile on every command
explicit --region on every command
STS identity check before every write
change ticket
maintenance window
Terraform freeze
CloudTrail
peer review
rollback owner
Never rely on:
AWS_PROFILE
alone for a high-impact migration if commands can easily run against the wrong account.
Explicitly specify:
--profile "$SOURCE_PROFILE"
or:
--profile "$DEST_PROFILE"
39. CloudTrail Audit
CloudTrail records API operations using:
eventSource = ec2.amazonaws.com
eventName = requested EC2 API operation
Important event names should therefore include:
EnableAddressTransfer
DisableAddressTransfer
AcceptAddressTransfer
DeleteNatGateway
CreateNatGateway
DisassociateAddress
AssociateAddress
ResetAddressAttribute
ModifyAddressAttribute
CreateTags
ReplaceRoute
Example:
aws cloudtrail lookup-events \
--lookup-attributes \
AttributeKey=EventName,AttributeValue=EnableAddressTransfer \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
Destination:
aws cloudtrail lookup-events \
--lookup-attributes \
AttributeKey=EventName,AttributeValue=AcceptAddressTransfer \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
CloudTrail supports searching recent management events by EventName.
40. Troubleshooting
| Problem | Likely cause | Validation | Fix |
|---|---|---|---|
| Transfer action unavailable | IAM/SCP/unsupported EIP | Check caller and EIP pool | Correct permissions / verify EIP type |
UnauthorizedOperation | IAM, boundary, SCP | STS + IAM/SCP review | Grant EC2 transfer action |
| Destination cannot accept | Still associated / PTR / quota | Inspect EIP, PTR, quota | Remove blocker |
AddressLimitExceeded | Destination EIP quota reached | Service Quotas | Increase quota/free EIP |
InvalidTransfer.AddressAssociated | EIP still attached | describe-addresses | Disassociate/delete NAT |
InvalidTransfer.AddressCustomPtrSet | PTR exists | describe-addresses-attribute | Reset PTR |
| Wrong account | Bad profile | sts get-caller-identity | Stop immediately |
| Wrong Region | Source/dest Region mismatch | explicit Region | Both must use ap-northeast-1 |
| Transfer expired | >7 days | describe-address-transfers | Re-enable transfer |
| NAT EIP will not detach | Primary EIP | NAT address inspection | Delete NAT gateway |
| Terraform wants new EIP | Old state/config mismatch | terraform plan | State reconcile/import |
| Terraform wants destroy | Wrong ownership/config | plan/state inspection | Stop; reconcile state |
| NAT creation fails | EIP/subnet/NBG/IGW/quota issue | NAT failure message | Correct target networking |
| Traffic has wrong outbound IP | Route uses wrong NAT | route + curl | Fix route |
| Vendor blocks traffic | Allowlist/session issue | vendor logs | Validate allowlist/connectivity |
AWS specifically documents the three major transfer-acceptance errors:
AddressLimitExceeded
InvalidTransfer.AddressCustomPtrSet
InvalidTransfer.AddressAssociated
41. Complete Command Cheat Sheet
Define:
export AWS_REGION=ap-northeast-1
export SOURCE_PROFILE=evp-staging-old
export DEST_PROFILE=evp-staging-new
export SOURCE_ACCOUNT_ID=111111111111
export DEST_ACCOUNT_ID=222222222222
export EIP=1.2.3.4
export SOURCE_ALLOCATION_ID=eipalloc-OLDXXXXXXXX
1. Verify source
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
2. Inspect EIP
aws ec2 describe-addresses \
--public-ips "$EIP" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
3. Inspect PTR
aws ec2 describe-addresses-attribute \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--attribute domain-name \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
4. Export tags
aws ec2 describe-addresses \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--query 'Addresses[0].Tags' \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE" \
> eip-tags.json
5. Enable transfer
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 enable-address-transfer \
--allocation-id "$SOURCE_ALLOCATION_ID" \
--transfer-account-id "$DEST_ACCOUNT_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
6. Verify pending
aws ec2 describe-address-transfers \
--allocation-ids "$SOURCE_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
7. Free source EIP
EC2/ENI:
aws ec2 disassociate-address \
--association-id "$ASSOCIATION_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
NAT primary EIP:
aws sts get-caller-identity \
--profile "$SOURCE_PROFILE"
aws ec2 delete-nat-gateway \
--nat-gateway-id "$SOURCE_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$SOURCE_PROFILE"
8. Confirm destination
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
9. Accept
aws ec2 accept-address-transfer \
--address "$EIP" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
10. Obtain NEW allocation ID
export DEST_ALLOCATION_ID="$(
aws ec2 describe-addresses \
--public-ips "$EIP" \
--query 'Addresses[0].AllocationId' \
--output text \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
)"
11. Verify
aws ec2 describe-addresses \
--allocation-ids "$DEST_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
12. Restore tags
aws ec2 create-tags \
--resources "$DEST_ALLOCATION_ID" \
--tags file://eip-tags.json \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
13. Create new NAT
aws sts get-caller-identity \
--profile "$DEST_PROFILE"
aws ec2 create-nat-gateway \
--subnet-id "$DEST_PUBLIC_SUBNET_ID" \
--allocation-id "$DEST_ALLOCATION_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
14. Update route
aws ec2 replace-route \
--route-table-id "$DEST_PRIVATE_ROUTE_TABLE_ID" \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id "$DEST_NAT_GW_ID" \
--region "$AWS_REGION" \
--profile "$DEST_PROFILE"
15. Test EKS
kubectl run eip-test \
--image=curlimages/curl \
--rm -it \
--restart=Never \
-- https://checkip.amazonaws.com
Expected:
1.2.3.4
42. Architecture Diagram
flowchart LR
subgraph ORGA["AWS Organization A"]
subgraph SRCA["Source AWS Account"]
VPC1["Source VPC"]
SUB1["Public Subnet"]
NAT1["NAT Gateway A"]
EIP1["EIP X<br/>1.2.3.4<br/>Old Allocation ID"]
VPC1 --> SUB1
SUB1 --> NAT1
NAT1 --> EIP1
end
end
EIP1 -->|"EIP ownership transfer<br/>Same Region"| EIP2
subgraph ORGB["AWS Organization B"]
subgraph DSTA["Destination AWS Account"]
VPC2["Destination VPC"]
SUB2["Public Subnet"]
NAT2["NAT Gateway B"]
EIP2["SAME EIP X<br/>1.2.3.4<br/>NEW Allocation ID"]
VPC2 --> SUB2
SUB2 --> NAT2
NAT2 --> EIP2
end
end
VPC1 -. "DOES NOT TRANSFER" .-> VPC2
NAT1 -. "DOES NOT TRANSFER" .-> NAT2
The transfer boundary is:
TRANSFERRED
───────────
Public IPv4 ownership
NOT TRANSFERRED
───────────────
VPC
NAT Gateway
Subnet
ENI
EC2
Route tables
DNS
tags
association
old allocation ID
43. Final Production Checklist
PREPARATION
[ ] Source account ID confirmed
[ ] Destination account ID confirmed
[ ] AWS Region confirmed as ap-northeast-1
[ ] Public EIP recorded
[ ] Source Allocation ID recorded
[ ] PublicIpv4Pool verified
[ ] Network Border Group verified
[ ] Association ID recorded
[ ] ENI/EC2 association checked
[ ] NAT Gateway association checked
[ ] NAT primary/secondary status checked
[ ] Source routes exported
[ ] Tags exported
[ ] PTR/reverse DNS checked
[ ] MongoDB Atlas allowlists documented
[ ] Confluent allowlists documented
[ ] Vendor allowlists documented
[ ] DNS dependencies documented
[ ] Destination EIP quota validated
[ ] Destination VPC ready
[ ] Destination public subnet ready
[ ] Destination Internet Gateway ready
[ ] Destination route tables ready
[ ] Destination Terraform ready
[ ] Source Terraform state backed up
[ ] Rollback procedure reviewed
[ ] Change window approved
TRANSFER
[ ] Terraform/apply freeze active
[ ] Source caller identity checked
[ ] EIP transfer enabled
[ ] Pending transfer verified
[ ] Destination account ID verified in transfer
[ ] Traffic drained where applicable
[ ] PTR removed if applicable
[ ] EC2/ENI EIP disassociated OR source NAT deleted
[ ] EIP confirmed free
[ ] Destination caller identity checked
[ ] Transfer accepted
[ ] Destination ownership verified
[ ] NEW Allocation ID recorded
TARGET
[ ] Tags recreated
[ ] Destination NAT created
[ ] NAT state = available
[ ] NAT shows SAME public EIP
[ ] Destination routes updated
[ ] Private subnet route correct
[ ] EKS outbound EIP verified
[ ] EC2 outbound EIP verified
[ ] MongoDB connectivity verified
[ ] Kafka connectivity verified
[ ] External APIs verified
[ ] Vendor allowlists verified
[ ] Application health verified
[ ] Monitoring healthy
IAC / CLEANUP
[ ] Source Terraform no longer manages EIP
[ ] Destination EIP imported using NEW Allocation ID
[ ] terraform plan clean in source
[ ] terraform plan clean in destination
[ ] Temporary NAT/EIP removed if applicable
[ ] Old route-table entries removed
[ ] Old infrastructure cleanup approved
[ ] CloudTrail evidence captured
[ ] Runbook updated
[ ] Change ticket completed
44. Final Decision Table
| Question | Answer |
|---|---|
| Cross-account transfer? | YES |
| Cross-Organization transfer? | YES |
| Same Organization required? | NO |
| Same Region required? | YES |
| Exact public IPv4 retained? | YES |
| Allocation ID retained? | NO — new allocation ID after transfer |
| Tags retained? | NO |
| Standard DNS records transferred? | NO |
| Existing EIP association transferred? | NO |
| EC2 transferred? | NO |
| ENI transferred? | NO |
| NAT Gateway transferred? | NO |
| VPC transferred? | NO |
| BYOIP transferable using this mechanism? | NO |
| Outposts CoIP transferable? | NO |
| PTR may remain during acceptance? | NO |
| Transfer expires? | YES — 7 days |
| Can pending transfer be cancelled? | YES |
| Can completed transfer simply be cancelled? | NO |
| Can ownership be transferred back afterward? | YES — perform another transfer |
| Can primary NAT EIP be manually removed? | NO |
| Does deleting NAT release the EIP? | NO |
| Does deleting NAT disassociate the EIP? | YES |
| True zero downtime with same sole NAT EIP? | No practical guarantee; design for minimum downtime |
Primary References
AWS EC2 — Elastic IP transfers:
AWS: Transfer an Elastic IP address between AWS accounts
AWS VPC — NAT gateway lifecycle:
AWS: Work with NAT gateways
AWS EC2 — Reverse DNS:
AWS: Reverse DNS for Elastic IP addresses
AWS IAM — EC2 actions:
AWS: Actions, resources and condition keys for Amazon EC2
AWS VPC quotas:
AWS: Amazon VPC quotas
AWS Knowledge Center — transfer verification/new AllocationId:
AWS: Transfer an Elastic IP between accounts
HashiCorp — aws_eip import/current provider behavior:
HashiCorp AWS Provider: aws_eip