Last Updated:

From AWS ClickOps to infrastructure-as-software - importing with Pulumi

Erik Lundevall-Zara
Erik Lundevall-Zara Article

Setting up infrastructure through ClickOps, even if we may not admit to it, is often a reality. ClickOps is setting up infrastructure through the web interface of the cloud provider, like AWS.

The AWS Console can be a confusing place sometimes, but is still quicker to test and try out things most times compared to infrastructure-as-code tools.

But for transforming that ClickOps-based effort into proper infrastructure-as-code, AWS is not helpful. So that part becomes a pain.

In this post I am taking Pulumi for a spin to see how it works out to import resources into a stack. Import, in this case, means taking existing resources that are not managed by an infrastructure-as-code tool into a managed state by that tool.

Why Pulumi? Besides Pulumi, both CloudFormation and Terraform have some support to import existing resources into a managed state/stack. However, that does not seem to extend to the tooling built on top of them for programming languages. Neither AWS Cloud Development Kit (AWs CDK) nor Cloud Development Kit for Terraform (CDKTF) seem to have any direct support for that. Pulumi has support that at least in theory can translate directly into infrastructure code.

The ClickOps start

To try out the import features, I picked what I think is a potential pain point for many - a VPC setup in AWS. I have had, and still have, multiple cases where a VPC setup either was done manually, or was changed manually after the initial infrastructure-as-code setup.

It is a pain point, because most times you cannot just tear it down and set up again, because there are many resources that depend on that VPC setup.

Also, it is not just a single resource, but several - the VPC logical component itself, multiple subnets, routing tables, routes, various gateways (internet gateway, NAT gateway, customer gateways), and probably some VPC endpoints to various AWS services, VPC peering connections, transit gateway, etc.

Even a relatively simple VPC setup has many resources in place. So this seems like an excellent starting point to see if the experience is painful or easy. In this case, I am focusing on importing something entirely from scratch.

To get something to work with, I signed in to the AWS Console, and on the VPC page started the VPC wizard. It is a pretty neat experience, but unfortunately it does not help with generating any infrastructure-as-code. That would have been too helpful.

The execution of the wizard provided me with these resources:

VPC wizard

This is still not a detailed view of all resources involved, but there are a few already to consider.

Let us get started!

A first look at import

There are a few ways to import existing resources into Pulumi, which include:

  • Direct import into a stack of a single resource via pulumi importcommand.
  • Direct import into a stack of a collection resources specified in a file, with pulumi import command.
  • Add import option to a resource in the code to reference a resource to import.

Details about importing can be found in the Pulumi documentation:

Import a single resource

Let us first try the option to import a single resource. A good start would be the VPC resource, which is the container for all the other VPC-related resources.

The format of the import command is essentially:

pulumi import pulumi_type name id

where pulumi_type is the internal type used by the provider, name the name the resource will have in the Pulumi code, and id the ID value the resource has in the cloud provider. For a VPC, that would be the vpc id.

The Pulumi type is in the Pulumi documentation for that provider, in this case in AWS. In there, one can find an import section where the information is:

https://www.pulumi.com/registry/packages/aws/api-docs/ec2/vpc/#import

What is my vpc id?

I can login through the AWS Console and find my VPC and look up the vpc id here. Or I can do that through a script. If I am going to find a lot of resources, I will probably not want to click around for every single resource to find the id and any other relevant information.

So let us get that through the command line.

One way is to use the AWS CLI:

> aws ec2 describe-vpcs --query 'Vpcs[?!IsDefault].{vpcid: VpcId, name: Tags[?Key == `Name`].Value | [0] }' --output table
-----------------------------------------------
|                DescribeVpcs                 |
+-------------------+-------------------------+
|       name        |          vpcid          |
+-------------------+-------------------------+
|  infra-import-vpc |  vpc-03328d2a6a2f4f3d9  |
+-------------------+-------------------------+

The --query option uses JMESPath, which can do a pretty good job extracting data from the data returned by the AWS CLI commands. However, if you do not use JMESPath regularly, it’s painful to get the query syntax right so you get what you want. I did not get that command right on the first attempt, far from it.

Another option which I like, is a 3rd party tool called Steampipe. It is a brilliant tool which allows you to run queries using regular SQL syntax (PostgreSQL flavour), towards cloud providers and other types of sources - not just AWS.

The same query with steampipe becomes:

❯ steampipe query "select vpc_id, tags->'Name' as name from aws_vpc where not is_default"
+-----------------------+--------------------+
| vpc_id                | name               |
+-----------------------+--------------------+
| vpc-03328d2a6a2f4f3d9 | "infra-import-vpc" |
+-----------------------+--------------------+

This one was easier and quicker to get right, in comparison.

Run the import

Now it is time to do the import:

pulumi import aws:ec2/vpc:Vpc my-vpc vpc-03328d2a6a2f4f3d9
    Type                 Name              Plan       
 +   pulumi:pulumi:Stack  import-infra-dev  create     
 =   └─ aws:ec2:Vpc       my-vpc            import     


Resources:
    + 1 to create
    = 1 to import
    2 changes

Do you want to perform this import? yes

That worked fine! The output reports success, plus it also generates the code to use for the imported resource, which is very neat!

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const my_vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    tags: {
        Name: "infra-import-vpc",
        Project: "InfraImport",
    },
}, {
    protect: true,
});

Now when I ran pulumi preview it reported no changes, so our code corresponds to what is in the state.

     Type                 Name              Plan     
     pulumi:pulumi:Stack  import-infra-dev           


Resources:
    2 unchanged

I can also run pulumi refresh to let Pulumi compare and potentially update the state with what is actually provisioned.


     Type                 Name              Plan     
     pulumi:pulumi:Stack  import-infra-dev           
     └─ aws:ec2:Vpc       my-vpc                     


Resources:
    2 unchanged

No changes here either, so everything should be just fine. So that was one of many resources. What about the rest?

Import by file

Instead of importing each resource one by one, Pulumi allows you to create a JSON file with resource references in it, so you can import multiple resources in one go.

Let us try that with the subnets in the VPC. First, let us get the subnet ids and some other info about the subnets for the VPC we have created.

> steampipe query "select subnet_id, availability_zone, cidr_block, tags from aws_vpc_subnet where vpc_id ='vpc-03328d2a6a2f4f3d9'"
+--------------------------+-------------------+---------------+----------------------------------------------------------------------------+
| subnet_id                | availability_zone | cidr_block    | tags                                                                       |
+--------------------------+-------------------+---------------+----------------------------------------------------------------------------+
| subnet-0fd83ae55a2f3b17b | eu-west-1b        | 10.0.16.0/20  | {"Name":"infra-import-subnet-public2-eu-west-1b","Project":"InfraImport"}  |
| subnet-0790e4f34196065ce | eu-west-1a        | 10.0.128.0/20 | {"Name":"infra-import-subnet-private1-eu-west-1a","Project":"InfraImport"} |
| subnet-0bd9953f3eff3c1cf | eu-west-1b        | 10.0.144.0/20 | {"Name":"infra-import-subnet-private2-eu-west-1b","Project":"InfraImport"} |
| subnet-05ea7216fa795af01 | eu-west-1a        | 10.0.0.0/20   | {"Name":"infra-import-subnet-public1-eu-west-1a","Project":"InfraImport"}  |
+--------------------------+-------------------+---------------+----------------------------------------------------------------------------+

The JSON file to use for import essentially comprises several resource entries with a type, name and id. I created a file with the subnets, and also the existing VPC. This is because I want to check what happens if I already have an imported resource in the file.

vpc-import.json

{
  "resources": [
    {
      "type": "aws:ec2/vpc:Vpc",
      "name": "my-vpc",
      "id": "vpc-03328d2a6a2f4f3d9"
    },
    {
      "type": "aws:ec2/subnet:Subnet",
      "name": "private-subnet1",
      "id": "subnet-0790e4f34196065ce"
    },
    {
      "type": "aws:ec2/subnet:Subnet",
      "name": "private-subnet2",
      "id": "subnet-0bd9953f3eff3c1cf"
    },
    {
      "type": "aws:ec2/subnet:Subnet",
      "name": "public-subnet1",
      "id": "subnet-05ea7216fa795af01"
    },
    {
      "type": "aws:ec2/subnet:Subnet",
      "name": "public-subnet2",
      "id": "subnet-0fd83ae55a2f3b17b"
    }
  ]
}

If we run pulumi import --file vpc-import.json on this file, we get this result (repeated diagnostics warnings are truncated):

     Type                 Name              Plan       Info
     pulumi:pulumi:Stack  import-infra-dev             
 =   ├─ aws:ec2:Subnet    private-subnet2   import     3 warnings
 =   ├─ aws:ec2:Subnet    private-subnet1   import     3 warnings
 =   ├─ aws:ec2:Subnet    public-subnet2    import     3 warnings
 =   └─ aws:ec2:Subnet    public-subnet1    import     3 warnings


Diagnostics:
  aws:ec2:Subnet (private-subnet1):
    warning: One or more imported inputs failed to validate. This is almost certainly a bug in the `aws` provider. The import will still proceed, but you will need to edit the generated code after copying it into your program.
    warning: aws:ec2/subnet:Subnet resource 'private-subnet1' has a problem: Conflicting configuration arguments: "availability_zone": conflicts with availability_zone_id. Examine values at 'Subnet.AvailabilityZone'.
    warning: aws:ec2/subnet:Subnet resource 'private-subnet1' has a problem: Conflicting configuration arguments: "availability_zone_id": conflicts with availability_zone. Examine values at 'Subnet.AvailabilityZoneId'.

This looks great for the VPC part. It knows it is already there and does not import that again. This should mean that we can tweak the file and run it again if needed. The diagnostics is perhaps more of a concern - there is an obvious conflict here, which may be a bug in the provider.

You have the option to perform the import (yes), abort (no), or get more details. If I choose to get more details, I am getting more details of the import (truncated):

Do you want to perform this import? details
  pulumi:pulumi:Stack: (same)
    [urn=urn:pulumi:dev::import-infra::pulumi:pulumi:Stack::import-infra-dev]
    = aws:ec2/subnet:Subnet: (import) 🔒
        [id=subnet-05ea7216fa795af01]
        [urn=urn:pulumi:dev::import-infra::aws:ec2/subnet:Subnet::public-subnet1]
        [provider=urn:pulumi:dev::import-infra::pulumi:providers:aws::default_5_21_1::8af5e02e-46e5-4c70-8040-ac25d52c85ba]
        availabilityZone              : "eu-west-1a"
        availabilityZoneId            : "euw1-az2"
        cidrBlock                     : "10.0.0.0/20"
        privateDnsHostnameTypeOnLaunch: "ip-name"
        tags                          : {
            Name      : "infra-import-subnet-public1-eu-west-1a"
            Project   : "InfraImport"
        }
        vpcId                         : "vpc-03328d2a6a2f4f3d9"```

As the diagnostics say, we need to edit the generated code after the import. That should be fine. Running the import, the generated code for the subnets has a pattern like this:

const private_subnet1 = new aws.ec2.Subnet("private-subnet1", {
    availabilityZone: "eu-west-1a",
    availabilityZoneId: "euw1-az2",
    cidrBlock: "10.0.128.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-private1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: "vpc-03328d2a6a2f4f3d9",
}, {
    protect: true,
});

Since there was a conflict between availabilityZone and availabilityZoneId will just remove the availabilityZoneId. Also, it seems the generated code does not do references via code objects, so we need to tweak the VPC id reference to not be hard-coded.

const private_subnet1 = new aws.ec2.Subnet("private-subnet1", {
    availabilityZone: "eu-west-1a",
    cidrBlock: "10.0.128.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-private1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

So running another pulumi preview shows that the code is ok:

Type                 Name              Plan     
     pulumi:pulumi:Stack  import-infra-dev           

Resources:
    6 unchanged

Next steps in import

So far, we have added resources one-by-one using pulumi import, and we have also manually created a JSON with resource references to import. It may become tedious to do, with many resources, in particular if we need to do similar imports multiple times.

Can we do this programmatrically? Pulumi does not have built-in support to do this currently. It is a trickly problem to solve, in particular if you want to cover all sorts of providers.

I wanted to try how far I get with some relatively simple code and logic. Since I had chosen Typescript as the target language for the examples, I used AWS SDK for JavaScript v3 to write some code to generate the expected JSON resource data Pulumi wants.

The code called AWS to get the VPC information and then used the obtained VPC for filtering which other resources to get, of different types. A part of the code looks like the code segment below.

The pattern is for each type of resource to:

  • Set up a filter for which resources to retrieve
  • Call AWS and wait for a response
  • Iterate over the returned resources and add an entry to the resource data array
export type ResourceData = {
  name: string;
  type: string;
  id: string;
};

const generateImportResources = async function() {
  const resources: ResourceData[] = [];

  const client = new EC2Client({ region: 'eu-west-1' });
  const vpcCommand = new DescribeVpcsCommand( { 
    Filters: [ 
      { Name: 'is-default', Values: [ 'false' ]}
    ]
  });

  try {
    const vpcData = await client.send(vpcCommand);
    if (!vpcData.Vpcs || vpcData.Vpcs.length === 0) {
      throw Error('No matching VPC');
    } 
    const vpcId = vpcData.Vpcs[0].VpcId;
    if (!vpcId) {
      throw Error('No VPC id');
    }
    resources.push({ name: 'my-vpc', type: 'aws:ec2/vpc:Vpc', id: vpcId});

    const subnetCommand = new DescribeSubnetsCommand({ 
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ]}
      ]
    });
    const subnetData = await client.send(subnetCommand);
    if (subnetData.Subnets) {
      for (const index in  subnetData.Subnets) {
        const subnet = subnetData.Subnets[index];
        if (subnet.SubnetId) resources.push({
          name: `subnet${index}`,
          type: 'aws:ec2/subnet:Subnet',
          id: subnet.SubnetId
        });
      }
    }

The complete code is at the end of this blog post.

Was this approach more efficient than doing it manually? No, not for a onetime use case. In the end, it was less than 20 resources, and I would have done it faster with a manual lookup.

Also, the code is not ready and done, since it does not reflect the larger context of the program. For example, the generated code for the default Network ACL (Access Control List) looks like this:

const networkacl0 = new aws.ec2.DefaultNetworkAcl("networkacl0", {
    defaultNetworkAclId: "acl-082d6b90a5a769977",
    egress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    ingress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    subnetIds: [
        "subnet-0fd83ae55a2f3b17b",
        "subnet-05ea7216fa795af01",
        "subnet-0790e4f34196065ce",
        "subnet-0bd9953f3eff3c1cf",
    ],
}, {
    protect: true,
});

which is not so good, because it will contain hard-coded values, instead of references to other resources in the same import. A slightly cleaned-up version would look like this:

const networkacl0 = new aws.ec2.DefaultNetworkAcl("networkacl0", {
    defaultNetworkAclId: vpc.defaultNetworkAclId,
    egress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    ingress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    subnetIds: [
        subnet3.id,
        subnet2.id,
        subnet1.id,
        subnet0.id,
    ],
}, {
    protect: true,
});

This is better, but still not that good. You would still need to work on code refactoring to clean up the code and make it look nicer.

Pulumi is still better than some other tools in this space, it is just we need to expect to do some manual work to clean up the code.

One thing we may want to do is to rename variables and resources, when we think of better ways to represent resources during refactoring. Renaming variables in the code itself may be fine, but resource names may be trickier, since that is stored in the stack state. If you rename a resource, Pulumi will think it is a new resource and will try to create a new one, and destroy the old one - which may not be what you want.

Fortunately, Pulumi has its alias feature, which allows you to rename resources and allows Pulumi to recognize that it is actually the same resource as before. In a simple case, we can rename a generated subnet resource to actually reflect that it is a public subnet:

const subnet1 = new aws.ec2.Subnet("subnet1", {
    availabilityZone: "eu-west-1a",
    cidrBlock: "10.0.0.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-public1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

We can rename the variable and the resource itself. For the variable, we just fix the code references as with any code. For the resource, we add an alias containing the old name.

const public_subnet1 = new aws.ec2.Subnet("public-subnet1", {
    availabilityZone: "eu-west-1a",
    cidrBlock: "10.0.0.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-public1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
    aliases: [ { name: 'subnet1'} ]
});

Terraform has something similar with their moved blocks, and CloudFormation has nothing like this.

Code before import

The final import approach we are going to test out is to write the code first, and then add an import to the code itself, to make Pulumi pick that up.

First, let us get brutal and explicitly delete a resource from the stack state. This is not something to do lightly, and this is mainly to illustrate the import feature in a simple way.

❯ pulumi state delete urn:pulumi:dev::import-infra::aws:ec2/vpc:Vpc::my-vpc --force
 warning: This command will edit your stack's state directly. Confirm? Yes
warning: deleting protected resource urn:pulumi:dev::import-infra::aws:ec2/vpc:Vpc::my-vpc due to presence of --force
Resource deleted

If we now run a pulumi preview the VPC resource is gone, and Pulumi wants to create it:

     Type                 Name              Plan       Info
     pulumi:pulumi:Stack  import-infra-dev             
 +   ├─ aws:ec2:Vpc       my-vpc            create     
     └─ aws:ec2:Subnet    subnet0                      1 error


Diagnostics:
  aws:ec2:Subnet (subnet0):
    error: unable to replace resource "urn:pulumi:dev::import-infra::aws:ec2/subnet:Subnet::subnet0"
    as it is currently marked for protection. To unprotect the resource, remove the `protect` flag from the resource in your Pulumi program and run `pulumi up`

So we can add an import option to the code that creates the VPC resource:

const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    tags: {
        Name: "infra-import-vpc",
        Project: "InfraImport",
    },
}, {
    protect: true,
    import: 'vpc-03328d2a6a2f4f3d9',
});

Now, if we run pulumi preview it recognizes that the resource should be imported instead. The preview itself does nothing though, so we should run pulumi up to actually trigger the import.

     Type                 Name              Plan       
     pulumi:pulumi:Stack  import-infra-dev             
 =   └─ aws:ec2:Vpc       my-vpc            import     


Resources:
    = 1 to import
    15 unchanged

After pulumi up it reports

     Type                 Name              Status            
     pulumi:pulumi:Stack  import-infra-dev                    
 =   └─ aws:ec2:Vpc       my-vpc            imported (1s)     


Resources:
    = 1 imported
    15 unchanged

Now it is safe to remove the import option from the VPC resource, since the resource has been imported properly.

I think this is a nice alternative to import resources, especially if I have a reasonably good idea how the code should look like. In that case, it might just be better to write the code first, then add the import option.

It is good to have multiple options to choose from.

If you would want to go with a code-first approach, writing the code may also take some time to get right. One way to do a quick start is to use Former2.

This is one of the brilliant tools that one would have wished AWS would have produced themselves. Former2 will allow you to provide some (read-only) credentials to scan resources in an account and select the resources you want code for. Former2 will then generate code that represents there resources.

It supports CloudFormation, Terraform, Troposphere, AWS CDK, CDK for Terraform, and Pulumi.

Right now, only Typescript is a supported language for Pulumi, and the output is like what pulumi import generates. A nice feature of Former2 is that detects dependencies. So it will ask you if you want to include these dependencies in the generated code as well.

Final words

Congratulations if you made it all the way here to the end!

I have tried to cover the different options available to import existing resources into Pulumi, given the specific case that these resources were created manually. When you have resources already managed by some other tool, like CloudFormation, Terraform, or AWS CDK, other considerations will apply as well.

Also, if you have a manually changed resource already managed by Pulumi, you can use pulumi refresh instead.

It will not be a completely automated process to import existing resources, but it is probably more manageable and pragmatic than with some of the other tools in this space.* I like that.

Appendix - code

import-vpc.ts

The code to generate the VPC resources JSON to use with pulumi import. Run it with

./node_modules/.bin/ts-node import-vpc.ts >vpc-import.json
import { 
  DescribeAddressesCommand,
  DescribeInternetGatewaysCommand,
  DescribeNatGatewaysCommand,
  DescribeNetworkAclsCommand,
  DescribeRouteTablesCommand,
  DescribeSecurityGroupsCommand,
  DescribeSubnetsCommand,
  DescribeVpcEndpointsCommand,
  DescribeVpcsCommand,
  EC2Client,
} from '@aws-sdk/client-ec2';

export type ResourceData = {
  name: string;
  type: string;
  id: string;
};

const generateImportResources = async function() {
  const resources: ResourceData[] = [];

  const client = new EC2Client({ region: 'eu-west-1' });
  const vpcCommand = new DescribeVpcsCommand( { 
    Filters: [ 
      { Name: 'is-default', Values: [ 'false' ]}
    ]
  });

  try {
    const vpcData = await client.send(vpcCommand);
    if (!vpcData.Vpcs || vpcData.Vpcs.length === 0) {
      throw Error('No matching VPC');
    } 
    const vpcId = vpcData.Vpcs[0].VpcId;
    if (!vpcId) {
      throw Error('No VPC id');
    }
    resources.push({ name: 'my-vpc', type: 'aws:ec2/vpc:Vpc', id: vpcId});

    const subnetCommand = new DescribeSubnetsCommand({ 
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ]}
      ]
    });
    const subnetData = await client.send(subnetCommand);
    if (subnetData.Subnets) {
      for (const index in  subnetData.Subnets) {
        const subnet = subnetData.Subnets[index];
        if (subnet.SubnetId) resources.push({
          name: `subnet${index}`,
          type: 'aws:ec2/subnet:Subnet',
          id: subnet.SubnetId
        });
      }
    }

    const routeTableCommand = new DescribeRouteTablesCommand({
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ] }
      ]
    });
    const routeTableData = await client.send(routeTableCommand);
    if (routeTableData.RouteTables) {
      for (const index in routeTableData.RouteTables) {
        const routeTable = routeTableData.RouteTables[index];
        if (routeTable.RouteTableId) resources.push({ 
          name: `routetable${index}`,
          type: 'aws:ec2/routeTable:RouteTable',
          id: routeTable.RouteTableId
        });
      }
    }

    const internetGatewayCommand = new DescribeInternetGatewaysCommand({
      Filters: [
        { Name: 'attachment.vpc-id', Values: [ vpcId ]}
      ]
    });
    const internetGatewayData = await client.send(internetGatewayCommand);
    if (internetGatewayData.InternetGateways) {
      for (const index in internetGatewayData.InternetGateways) {
        const internetGateway = internetGatewayData.InternetGateways[index];
        if (internetGateway.InternetGatewayId) 
          resources.push({ 
            name: `internet-gateway${index}`,
            type: 'aws:ec2/internetGateway:InternetGateway',
            id: internetGateway.InternetGatewayId
          });
      }
    }

    const natGatewayCommand = new DescribeNatGatewaysCommand({
      Filter: [
        { Name: 'vpc-id', Values: [ vpcId ]}
      ]
    });
    const natGatewayData = await client.send(natGatewayCommand);
    if (natGatewayData.NatGateways) {
      for (const index in natGatewayData.NatGateways) {
        const natGateway = natGatewayData.NatGateways[index];
        if (natGateway.NatGatewayId) resources.push({
          name: `nat-gateway${index}`,
          type: 'aws:ec2/natGateway:NatGateway',
          id: natGateway.NatGatewayId
        });
      }
    }

    const eipCommand = new DescribeAddressesCommand({});
    const eipData = await client.send(eipCommand);
    if (eipData.Addresses) {
      for (const index in eipData.Addresses) {
        const eip = eipData.Addresses[index];
        if (eip.AllocationId) resources.push({
          name: `eip${index}`,
          type: 'aws:ec2/eip:Eip',
          id: eip.AllocationId
        });
      }
    }

    const netAclCommand = new DescribeNetworkAclsCommand({
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ]},
        { Name: 'default', Values: [ 'true' ]}
      ]
    });
    const netAclData = await client.send(netAclCommand);
    if (netAclData.NetworkAcls) {
      for (const index in netAclData.NetworkAcls) {
        const netAcl = netAclData.NetworkAcls[index];
        if (netAcl.NetworkAclId) resources.push({
          name: `networkacl${index}`,
          type: 'aws:ec2/defaultNetworkAcl:DefaultNetworkAcl',
          id: netAcl.NetworkAclId
        });
      }
    }

    const securityGroupCommand = new DescribeSecurityGroupsCommand({
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ]},
        { Name: 'group-name', Values: [ 'default' ]},
      ]
    });
    const securityGroupData = await client.send(securityGroupCommand);
    if (securityGroupData.SecurityGroups) {
      for (const index in securityGroupData.SecurityGroups) {
        const securityGroup = securityGroupData.SecurityGroups[index];
        if (securityGroup.GroupId) resources.push({
          name: `defaultsecuritygroup${index}`,
          type: 'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
          id: securityGroup.GroupId
        });
      }
    }

    const vpcEndpointCommand = new DescribeVpcEndpointsCommand({
      Filters: [
        { Name: 'vpc-id', Values: [ vpcId ]}
      ]
    });
    const vpcEndpointData = await client.send(vpcEndpointCommand);
    if (vpcEndpointData.VpcEndpoints) {
      for (const index in vpcEndpointData.VpcEndpoints) {
        const vpcEndpoint = vpcEndpointData.VpcEndpoints[index];
        if (vpcEndpoint.VpcEndpointId) resources.push({
          name: `vpc-endpoint${index}`,
          type: 'aws:ec2/vpcEndpoint:VpcEndpoint',
          id: vpcEndpoint.VpcEndpointId
        });
      }
    }

  } catch (error) {
    console.error(error);
  } finally {
    console.log(JSON.stringify({ resources }, null, 2));
  };
}

generateImportResources();

index.ts

The Pulumi code for the VPC resources after import and some minor modifications of the code.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    tags: {
        Name: "infra-import-vpc",
        Project: "InfraImport",
    },
}, {
    protect: true,
});

const subnet3 = new aws.ec2.Subnet("subnet3", {
    availabilityZone: "eu-west-1a",
    cidrBlock: "10.0.128.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-private1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const subnet2 = new aws.ec2.Subnet("subnet2", {
    availabilityZone: "eu-west-1b",
    cidrBlock: "10.0.144.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-private2-eu-west-1b",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const public_subnet1 = new aws.ec2.Subnet("public-subnet1", {
    availabilityZone: "eu-west-1a",
    cidrBlock: "10.0.0.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-public1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
    aliases: [ { name: 'subnet1'} ]
});

const subnet0 = new aws.ec2.Subnet("subnet0", {
    availabilityZone: "eu-west-1b",
    cidrBlock: "10.0.16.0/20",
    privateDnsHostnameTypeOnLaunch: "ip-name",
    tags: {
        Name: "infra-import-subnet-public2-eu-west-1b",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const networkacl0 = new aws.ec2.DefaultNetworkAcl("networkacl0", {
    defaultNetworkAclId: vpc.defaultNetworkAclId,
    egress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    ingress: [{
        action: "allow",
        cidrBlock: "0.0.0.0/0",
        fromPort: 0,
        protocol: "-1",
        ruleNo: 100,
        toPort: 0,
    }],
    subnetIds: [
        subnet3.id,
        subnet2.id,
        public_subnet1.id,
        subnet0.id,
    ],
}, {
    protect: true,
});
const internet_gateway0 = new aws.ec2.InternetGateway("internet-gateway0", {
    tags: {
        Name: "infra-import-igw",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const defaultsecuritygroup0 = new aws.ec2.DefaultSecurityGroup("defaultsecuritygroup0", {
    egress: [{
        cidrBlocks: ["0.0.0.0/0"],
        fromPort: 0,
        protocol: "-1",
        toPort: 0,
    }],
    ingress: [{
        fromPort: 0,
        protocol: "-1",
        self: true,
        toPort: 0,
    }],
    vpcId: vpc.id,
}, {
    protect: true,
});


const routetable3 = new aws.ec2.RouteTable("routetable3", {
    routes: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: internet_gateway0.id,
    }],
    tags: {
        Name: "infra-import-rtb-public",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});


const eip0 = new aws.ec2.Eip("eip0", {
    networkBorderGroup: "eu-west-1",
    publicIpv4Pool: "amazon",
    tags: {
        Name: "infra-import-eip-eu-west-1a",
        Project: "InfraImport",
    },
    vpc: true,
}, {
    protect: true,
});

const nat_gateway0 = new aws.ec2.NatGateway("nat-gateway0", {
    allocationId: eip0.allocationId,
    subnetId: public_subnet1.id,
    tags: {
        Name: "infra-import-nat-public1-eu-west-1a",
        Project: "InfraImport",
    },
}, {
    protect: true,
});

const routetable0 = new aws.ec2.RouteTable("routetable0", {vpcId: vpc.id}, {
    protect: true,
});
const routetable1 = new aws.ec2.RouteTable("routetable1", {
    routes: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: nat_gateway0.id,
    }],
    tags: {
        Name: "infra-import-rtb-private2-eu-west-1b",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const routetable2 = new aws.ec2.RouteTable("routetable2", {
    routes: [{
        cidrBlock: "0.0.0.0/0",
        natGatewayId: nat_gateway0.id,
    }],
    tags: {
        Name: "infra-import-rtb-private1-eu-west-1a",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});

const vpc_endpoint0 = new aws.ec2.VpcEndpoint("vpc-endpoint0", {
    policy: "{\"Statement\":[{\"Action\":\"*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2008-10-17\"}",
    routeTableIds: [
        routetable1.id,
        routetable2.id,
    ],
    serviceName: "com.amazonaws.eu-west-1.s3",
    tags: {
        Name: "infra-import-vpce-s3",
        Project: "InfraImport",
    },
    vpcId: vpc.id,
}, {
    protect: true,
});