diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 32b9f351f..26eab6e87 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -26,6 +26,7 @@ MODEL_MAP = {
"AWS::EC2::EIP": ec2_models.ElasticAddress,
"AWS::EC2::Instance": ec2_models.Instance,
"AWS::EC2::InternetGateway": ec2_models.InternetGateway,
+ "AWS::EC2::NatGateway": ec2_models.NatGateway,
"AWS::EC2::NetworkInterface": ec2_models.NetworkInterface,
"AWS::EC2::Route": ec2_models.Route,
"AWS::EC2::RouteTable": ec2_models.RouteTable,
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index 1c9e34989..db2b71bcb 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -15,6 +15,7 @@ from boto.ec2.launchspecification import LaunchSpecification
from moto.core import BaseBackend
from moto.core.models import Model
+from moto.core.utils import iso_8601_datetime_with_milliseconds
from .exceptions import (
EC2ClientError,
DependencyViolationError,
@@ -70,6 +71,7 @@ from .utils import (
random_instance_id,
random_internet_gateway_id,
random_ip,
+ random_nat_gateway_id,
random_key_pair,
random_private_ip,
random_public_ip,
@@ -2955,6 +2957,70 @@ class CustomerGatewayBackend(object):
return deleted
+class NatGateway(object):
+
+ def __init__(self, backend, subnet_id, allocation_id):
+ # public properties
+ self.id = random_nat_gateway_id()
+ self.subnet_id = subnet_id
+ self.allocation_id = allocation_id
+ self.state = 'available'
+ self.private_ip = random_private_ip()
+
+ # protected properties
+ self._created_at = datetime.utcnow()
+ self._backend = backend
+ # NOTE: this is the core of NAT Gateways creation
+ self._eni = self._backend.create_network_interface(backend.get_subnet(self.subnet_id), self.private_ip)
+
+ # associate allocation with ENI
+ self._backend.associate_address(eni=self._eni, allocation_id=self.allocation_id)
+
+ @property
+ def vpc_id(self):
+ subnet = self._backend.get_subnet(self.subnet_id)
+ return subnet.vpc_id
+
+ @property
+ def create_time(self):
+ return iso_8601_datetime_with_milliseconds(self._created_at)
+
+ @property
+ def network_interface_id(self):
+ return self._eni.id
+
+ @property
+ def public_ip(self):
+ eips = self._backend.address_by_allocation([self.allocation_id])
+ return eips[0].public_ip
+
+ @classmethod
+ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ ec2_backend = ec2_backends[region_name]
+ nat_gateway = ec2_backend.create_nat_gateway(
+ cloudformation_json['Properties']['SubnetId'],
+ cloudformation_json['Properties']['AllocationId'],
+ )
+ return nat_gateway
+
+
+class NatGatewayBackend(object):
+
+ def __init__(self):
+ self.nat_gateways = {}
+
+ def get_all_nat_gateways(self, filters):
+ return self.nat_gateways.values()
+
+ def create_nat_gateway(self, subnet_id, allocation_id):
+ nat_gateway = NatGateway(self, subnet_id, allocation_id)
+ self.nat_gateways[nat_gateway.id] = nat_gateway
+ return nat_gateway
+
+ def delete_nat_gateway(self, nat_gateway_id):
+ return self.nat_gateways.pop(nat_gateway_id)
+
+
class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend,
RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend,
VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend,
@@ -2963,7 +3029,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend,
RouteTableBackend, RouteBackend, InternetGatewayBackend,
VPCGatewayAttachmentBackend, SpotRequestBackend,
ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend,
- NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend):
+ NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend,
+ NatGatewayBackend):
def __init__(self, region_name):
super(EC2Backend, self).__init__()
diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py
index e51992e41..d939178fb 100644
--- a/moto/ec2/responses/__init__.py
+++ b/moto/ec2/responses/__init__.py
@@ -29,6 +29,7 @@ from .vpcs import VPCs
from .vpc_peering_connections import VPCPeeringConnections
from .vpn_connections import VPNConnections
from .windows import Windows
+from .nat_gateways import NatGateways
class EC2Response(
@@ -61,6 +62,7 @@ class EC2Response(
VPCPeeringConnections,
VPNConnections,
Windows,
+ NatGateways,
):
@property
def ec2_backend(self):
diff --git a/moto/ec2/responses/nat_gateways.py b/moto/ec2/responses/nat_gateways.py
new file mode 100644
index 000000000..98d383d47
--- /dev/null
+++ b/moto/ec2/responses/nat_gateways.py
@@ -0,0 +1,73 @@
+from __future__ import unicode_literals
+from moto.core.responses import BaseResponse
+from moto.ec2.utils import filters_from_querystring
+
+
+class NatGateways(BaseResponse):
+
+ def create_nat_gateway(self):
+ subnet_id = self._get_param('SubnetId')
+ allocation_id = self._get_param('AllocationId')
+ nat_gateway = self.ec2_backend.create_nat_gateway(subnet_id=subnet_id, allocation_id=allocation_id)
+ template = self.response_template(CREATE_NAT_GATEWAY)
+ return template.render(nat_gateway=nat_gateway)
+
+ def delete_nat_gateway(self):
+ nat_gateway_id = self._get_param('NatGatewayId')
+ nat_gateway = self.ec2_backend.delete_nat_gateway(nat_gateway_id)
+ template = self.response_template(DELETE_NAT_GATEWAY_RESPONSE)
+ return template.render(nat_gateway=nat_gateway)
+
+ def describe_nat_gateways(self):
+ filters = filters_from_querystring(self.querystring)
+ nat_gateways = self.ec2_backend.get_all_nat_gateways(filters)
+ template = self.response_template(DESCRIBE_NAT_GATEWAYS_RESPONSE)
+ return template.render(nat_gateways=nat_gateways)
+
+
+DESCRIBE_NAT_GATEWAYS_RESPONSE = """
+ bfed02c6-dae9-47c0-86a2-example
+
+ {% for nat_gateway in nat_gateways %}
+ -
+ {{ nat_gateway.subnet_id }}
+
+
-
+ {{ nat_gateway.network_interface_id }}
+ {{ nat_gateway.public_ip }}
+ {{ nat_gateway.allocation_id }}
+ {{ nat_gateway.private_ip }}
+
+
+ {{ nat_gateway.create_time }}
+ {{ nat_gateway.vpc_id }}
+ {{ nat_gateway.id }}
+ {{ nat_gateway.state }}
+
+ {% endfor %}
+
+
+"""
+
+CREATE_NAT_GATEWAY = """
+ 1b74dc5c-bcda-403f-867d-example
+
+ {{ nat_gateway.subnet_id }}
+
+ -
+ {{ nat_gateway.allocation_id }}
+
+
+ {{ nat_gateway.create_time }}
+ {{ nat_gateway.vpc_id }}
+ {{ nat_gateway.id }}
+ {{ nat_gateway.state }}
+
+
+"""
+
+
+DELETE_NAT_GATEWAY_RESPONSE = """
+ 741fc8ab-6ebe-452b-b92b-example
+ {{ nat_gateway.id }}
+"""
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 5b7743bf4..5b05cafa0 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -9,6 +9,7 @@ EC2_RESOURCE_TO_PREFIX = {
'image': 'ami',
'instance': 'i',
'internet-gateway': 'igw',
+ 'nat-gateway': 'nat',
'network-acl': 'acl',
'network-acl-subnet-assoc': 'aclassoc',
'network-interface': 'eni',
@@ -33,8 +34,7 @@ EC2_RESOURCE_TO_PREFIX = {
EC2_PREFIX_TO_RESOURCE = dict((v, k) for (k, v) in EC2_RESOURCE_TO_PREFIX.items())
-def random_id(prefix=''):
- size = 8
+def random_id(prefix='', size=8):
chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f']
resource_id = ''.join(six.text_type(random.choice(chars)) for x in range(size))
@@ -133,6 +133,10 @@ def random_eni_attach_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX['network-interface-attachment'])
+def random_nat_gateway_id():
+ return random_id(prefix=EC2_RESOURCE_TO_PREFIX['nat-gateway'], size=17)
+
+
def random_public_ip():
return '54.214.{0}.{1}'.format(random.choice(range(255)),
random.choice(range(255)))
diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py
index 4a7ff7f77..5f52bb9a9 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_integration.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py
@@ -1526,3 +1526,65 @@ def test_lambda_function():
result['Functions'][0]['MemorySize'].should.equal(128)
result['Functions'][0]['Role'].should.equal('test-role')
result['Functions'][0]['Runtime'].should.equal('nodejs')
+
+
+@mock_cloudformation
+@mock_ec2
+def test_nat_gateway():
+ ec2_conn = boto3.client('ec2', 'us-east-1')
+ vpc_id = ec2_conn.create_vpc(CidrBlock="10.0.0.0/16")['Vpc']['VpcId']
+ subnet_id = ec2_conn.create_subnet(CidrBlock='10.0.1.0/24', VpcId=vpc_id)['Subnet']['SubnetId']
+ route_table_id = ec2_conn.create_route_table(VpcId=vpc_id)['RouteTable']['RouteTableId']
+
+ template = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "NAT" : {
+ "DependsOn" : "vpcgatewayattachment",
+ "Type" : "AWS::EC2::NatGateway",
+ "Properties" : {
+ "AllocationId" : { "Fn::GetAtt" : ["EIP", "AllocationId"]},
+ "SubnetId" : subnet_id
+ }
+ },
+ "EIP" : {
+ "Type" : "AWS::EC2::EIP",
+ "Properties" : {
+ "Domain" : "vpc"
+ }
+ },
+ "Route" : {
+ "Type" : "AWS::EC2::Route",
+ "Properties" : {
+ "RouteTableId" : route_table_id,
+ "DestinationCidrBlock" : "0.0.0.0/0",
+ "NatGatewayId" : { "Ref" : "NAT" }
+ }
+ },
+ "internetgateway": {
+ "Type": "AWS::EC2::InternetGateway"
+ },
+ "vpcgatewayattachment": {
+ "Type": "AWS::EC2::VPCGatewayAttachment",
+ "Properties": {
+ "InternetGatewayId": {
+ "Ref": "internetgateway"
+ },
+ "VpcId": vpc_id,
+ },
+ }
+ }
+ }
+
+ cf_conn = boto3.client('cloudformation', 'us-east-1')
+ cf_conn.create_stack(
+ StackName="test_stack",
+ TemplateBody=json.dumps(template),
+ )
+
+ result = ec2_conn.describe_nat_gateways()
+
+ result['NatGateways'].should.have.length_of(1)
+ result['NatGateways'][0]['VpcId'].should.equal(vpc_id)
+ result['NatGateways'][0]['SubnetId'].should.equal(subnet_id)
+ result['NatGateways'][0]['State'].should.equal('available')
diff --git a/tests/test_ec2/test_nat_gateway.py b/tests/test_ec2/test_nat_gateway.py
new file mode 100644
index 000000000..d073fc5e3
--- /dev/null
+++ b/tests/test_ec2/test_nat_gateway.py
@@ -0,0 +1,100 @@
+from __future__ import unicode_literals
+import boto3
+import sure # noqa
+from moto import mock_ec2
+
+
+@mock_ec2
+def test_describe_nat_gateways():
+ conn = boto3.client('ec2', 'us-east-1')
+
+ response = conn.describe_nat_gateways()
+
+ response['NatGateways'].should.have.length_of(0)
+
+
+@mock_ec2
+def test_create_nat_gateway():
+ conn = boto3.client('ec2', 'us-east-1')
+ vpc = conn.create_vpc(CidrBlock='10.0.0.0/16')
+ vpc_id = vpc['Vpc']['VpcId']
+ subnet = conn.create_subnet(
+ VpcId=vpc_id,
+ CidrBlock='10.0.1.0/27',
+ AvailabilityZone='us-east-1a',
+ )
+ allocation_id = conn.allocate_address(Domain='vpc')['AllocationId']
+ subnet_id = subnet['Subnet']['SubnetId']
+
+ response = conn.create_nat_gateway(
+ SubnetId=subnet_id,
+ AllocationId=allocation_id,
+ )
+
+ response['NatGateway']['VpcId'].should.equal(vpc_id)
+ response['NatGateway']['SubnetId'].should.equal(subnet_id)
+ response['NatGateway']['State'].should.equal('available')
+
+
+@mock_ec2
+def test_delete_nat_gateway():
+ conn = boto3.client('ec2', 'us-east-1')
+ vpc = conn.create_vpc(CidrBlock='10.0.0.0/16')
+ vpc_id = vpc['Vpc']['VpcId']
+ subnet = conn.create_subnet(
+ VpcId=vpc_id,
+ CidrBlock='10.0.1.0/27',
+ AvailabilityZone='us-east-1a',
+ )
+ allocation_id = conn.allocate_address(Domain='vpc')['AllocationId']
+ subnet_id = subnet['Subnet']['SubnetId']
+
+ nat_gateway = conn.create_nat_gateway(
+ SubnetId=subnet_id,
+ AllocationId=allocation_id,
+ )
+ nat_gateway_id = nat_gateway['NatGateway']['NatGatewayId']
+ response = conn.delete_nat_gateway(NatGatewayId=nat_gateway_id)
+
+ response.should.equal({
+ 'NatGatewayId': nat_gateway_id,
+ 'ResponseMetadata': {
+ 'HTTPStatusCode': 200,
+ 'RequestId': '741fc8ab-6ebe-452b-b92b-example'
+ }
+ })
+
+
+@mock_ec2
+def test_create_and_describe_nat_gateway():
+ conn = boto3.client('ec2', 'us-east-1')
+ vpc = conn.create_vpc(CidrBlock='10.0.0.0/16')
+ vpc_id = vpc['Vpc']['VpcId']
+ subnet = conn.create_subnet(
+ VpcId=vpc_id,
+ CidrBlock='10.0.1.0/27',
+ AvailabilityZone='us-east-1a',
+ )
+ allocation_id = conn.allocate_address(Domain='vpc')['AllocationId']
+ subnet_id = subnet['Subnet']['SubnetId']
+
+ create_response = conn.create_nat_gateway(
+ SubnetId=subnet_id,
+ AllocationId=allocation_id,
+ )
+ nat_gateway_id = create_response['NatGateway']['NatGatewayId']
+ describe_response = conn.describe_nat_gateways()
+
+ enis = conn.describe_network_interfaces()['NetworkInterfaces']
+ eni_id = enis[0]['NetworkInterfaceId']
+ public_ip = conn.describe_addresses(AllocationIds=[allocation_id])['Addresses'][0]['PublicIp']
+
+ describe_response['NatGateways'].should.have.length_of(1)
+ describe_response['NatGateways'][0]['NatGatewayId'].should.equal(nat_gateway_id)
+ describe_response['NatGateways'][0]['State'].should.equal('available')
+ describe_response['NatGateways'][0]['SubnetId'].should.equal(subnet_id)
+ describe_response['NatGateways'][0]['VpcId'].should.equal(vpc_id)
+ describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['AllocationId'].should.equal(allocation_id)
+ describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['NetworkInterfaceId'].should.equal(eni_id)
+ assert describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PrivateIp'].startswith('10.')
+ describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PublicIp'].should.equal(public_ip)