Terraform conditionally create VPC
The way I'm understanding your question is that you're writing a module that takes an optional vpc_id as an argument, and either uses it if specified or creates a new VPC itself if not specified. You then need to use the VPC id elsewhere in your configuration, regardless of whether it was provided by the caller directly or if it was created by the VPC module.

If I understood that correctly, here's one way to get that done.

First, we'll declare the vpc_id variable for your own module that the rest of this will use to make its decisions.

variable "vpc_id" {
  type    = string
  default = null # optional with no default value
}


From this we will derive our decision about whether to create the VPC, which I'll write as a local value here mainly for exposition purposes but you might choose to inline it at the point of use in practice depending on what feels most readable for you:

locals {
  create_vpc = var.vpc_id != null ? true : false
}


We can now use that value both with the module and with a data resource so that we'll either create a new VPC or retrieve data about the existing one:

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  create_vpc = local.create_vpc
  # ...
}

data "aws_vpc" "selected" {
  count = local.create_vpc ? 0 : 1

  id = var.vpc_id
}


We can then write another local value that chooses which of those two locations to derive other VPC-related values, such as the id and cidr_block from:

locals {
  vpc = (
    local.create_vpc ?
    {
      id         = module.vpc.vpc_id
      cidr_block = module.vpc.vpc_cidr_block
    } :
    {
      id         = data.aws_vpc.selected.id
      cidr_block = data.aws_vpc.selected.cidr_block
    }
  )
}


You can then use local.vpc.id and local.vpc.cidr_block elsewhere in the configuration to get the appropriate values to use regardless of whether this module created its own VPC or not.



The above is the most direct answer to your question. I also wanted to share a different approach that is based on the patterns described in the Module Composition section of Terraform's docs, which might be appropriate for you if you're building a shared module intended to be called by another Terraform module.

A different way to approach this problem using composition is to say that your module never creates its own VPC, and always expects one to be provided to it. This is an example of the dependency inversion principle. Here's how that might look for your particular use-case...

In your module, you can declare an input variable called vpc that has the same structure as the final local.vpc that we created in the previous approach:

variable "vpc" {
  type = object({
    id         = string
    cidr_block = string
  })
  # This time the VPC is required. The caller decides how to obtain it.
}


Your own module's expressions would then get the VPC id as var.vpc.id instead of local.vpc.id as in the previous approach.

The calling module can then decide between several different ways of providing the value for that variable:

# Using the terraform-aws-modules/vpc/aws module
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  # ...
}

module "your_module" {
  source = "./modules/your-module"

  vpc = {
    id         = module.vpc.vpc_id
    cidr_block = module.vpc.vpc_cidr_block
  }
}


# Directly using the aws_vpc resource type

resource "aws_vpc" "example" {
  # ...
}

module "your_module" {
  source = "./modules/your-module"

  vpc = aws_vpc.example
}


# Using the aws_vpc data source to find an existing VPC

data "aws_vpc" "example" {
  tags = {
    Environment = "STAGE"
  }
}

module "your_module" {
  source = "./modules/your-module"

  vpc = data.aws_vpc.example
}


In other words, this lets your new module focus on whatever new problem it is trying to solve while being decoupled from the problem of creating a VPC, allowing the calling module to obtain that VPC information in whatever way is appropriate. The appropriate way to obtain the VPC might change over time, and this composition approach means that callers can have more control over how they manage the transition between different approaches, independent of the implementation of your new module.

Module composition isn't always the best option, but I wanted to point it out because the Terraform documentation specifically recommends considering this approach and recommends against having many levels of nested module calls.