Terraform Troubleshooting - DevWithNehaCloud/Oracle-Cloud-Infrastructure-wiki GitHub Wiki

Terraform Working Directory Behavior and Troubleshooting

Important Terraform Behavior

Terraform does not run a specific .tf file.

Instead, Terraform reads and combines all .tf files present in the current working directory and treats them as a single configuration.

Example

Suppose the current directory contains:

instance.tf
instance2.tf
network.tf
security.tf

When you run:

terraform plan

or

terraform apply

Terraform will read all four files and process every resource defined in them.

Terraform does not execute only the file you are currently editing.


Example Scenario

instance.tf

resource "oci_core_instance" "server1" {
  ...
}

instance2.tf

resource "oci_core_instance" "server2" {
  ...
}

network.tf

resource "oci_core_vcn" "vcn1" {
  ...
}

When you run:

terraform plan

Terraform evaluates all resources together:

  • server1
  • server2
  • vcn1

Even if you were only making changes in instance2.tf.


Why Did Terraform Show "4 to Add"?

The directory contained two instance resources:

instance.tf

resource "oci_core_instance" "generated_oci_core_instance" {
  ...
}

instance2.tf

resource "oci_core_instance" "server" {
  count = 3
  ...
}

Terraform calculated:

generated_oci_core_instance = 1 instance

server[count=3] = 3 instances

Total = 4 instances

Therefore the plan displayed:

Plan: 4 to add, 0 to change, 0 to destroy.

How to Work with a Separate Terraform Configuration

If you want Terraform to process only a specific configuration:

Option 1: Create a Separate Directory

mkdir ThreeInstanceTest
cd ThreeInstanceTest

Copy only the required Terraform files into that directory.

Example:

cp ~/instance2.tf .

Then run:

terraform init
terraform plan
terraform apply

Terraform will process only the files present in that directory.


Option 2: Rename Unused Files

Terraform only reads files with the .tf extension.

Example:

mv instance.tf instance.tf.bak

Now Terraform will ignore instance.tf.


Provider Configuration in a New Directory

When creating a new Terraform project directory, the OCI provider configuration must also be present.

Example:

provider "oci" {
  auth             ="APIKey"
  tenancy_ocid     = "<TENANCY_OCID>"
  user_ocid        = "<USER_OCID>"
  private_key_path = "/home/ngsoftlook/privkey.pem"
  fingerprint      = "<FINGERPRINT>"
  region           = "us-ashburn-1"
}

If the provider block is missing, Terraform may fail with errors such as:

Error: can not create client, bad configuration:
did not find a proper configuration for key id

Key Learnings

  • Terraform processes all .tf files in the current directory.
  • Terraform does not execute a single .tf file independently.
  • Resources from multiple .tf files are combined into one configuration.
  • A new working directory requires its own provider configuration.
  • Files that do not have the .tf extension are ignored by Terraform.
  • Using count allows multiple resources to be created from a single resource block.
  • Always review the output of terraform plan before running terraform apply.

If want to create 3 instance at once then add these lines in the code

image

save the file and then again run terraform plan and terraform apply.

image

Note - in this instance case name would be - terraform-instance-1, terraform-instance-2, terraform-instance-3

If you want to change(update the name of that instances not recreate the instances)then the code would be(before the resource block)

image image

If you want to recreate these 3 servers (in new .tf file copy entire content) and change these lines in the code

image image image

Provider Configuration Best Practice in Terraform

Provider configuration should always be defined in the Root Module, not inside child modules.

Recommended Structure

dev/
├── provider.tf
├── instance.tf
├── variables.tf
└── outputs.tf

instance_module/
├── main.tf
├── variables.tf
└── outputs.tf

In the above example:

  • dev is the Root Module.
  • instance_module is a Child Module.
  • The OCI provider configuration must be placed in the Root Module (dev/provider.tf).

Example

provider "oci" {
  auth             = "APIKey"
  tenancy_ocid     = var.tenancy_ocid
  user_ocid        = var.user_ocid
  private_key_path = var.private_key_path
  fingerprint      = var.fingerprint
  region           = var.region
}

Why?

When Terraform runs from a directory, it expects provider configurations to be available in the Root Module. Child modules inherit the provider configuration from the Root Module.

If the provider configuration is defined only inside a child module, Terraform may fail with errors such as:

## Provider "registry.terraform.io/hashicorp/oci" requires explicit configuration.

Key Takeaway

The provider configuration does not need to be in the same file as the resource definition. However, it should be present in the Root Module from which Terraform commands are executed.

Benefits of Keeping provider.tf in Each Environment

Environment Independence

Each environment can have its own:

  • OCI region
  • OCI user
  • API key
  • Authentication configuration

without affecting other environments.


Easier Troubleshooting

Provider-related issues can be isolated to a specific environment.

For example:

dev/provider.tf
test/provider.tf
uat/provider.tf
prod/provider.tf

If a configuration issue occurs in Dev, it can be fixed without impacting Test, UAT, or Production.


Better Reusability

The same module can be reused by multiple environments without modification.

module "instance" {
  source = "../instance_module"
}

The module remains generic and reusable because provider details are managed by the calling environment.


Alignment with Terraform Best Practices

Terraform recommends keeping provider configurations in the root module and keeping child modules provider-agnostic whenever possible.

This makes the infrastructure code easier to maintain and reuse across multiple environments.


Lesson Learned

During implementation, provider-related issues were encountered when provider.tf existed only inside the instance_module directory.

The issue was resolved by moving the provider configuration to each environment directory (dev, test, uat, and prod) and allowing the environments to call the shared module.

This resulted in:

  • Cleaner architecture
  • Better environment isolation
  • Easier troubleshooting
  • Improved module reusability
  • Compliance with Terraform best practices

Conclusion

Although it may seem convenient to keep provider.tf inside the shared module, Terraform works more effectively when provider configurations are defined in the root module (environment directory) and modules are used only for reusable resource definitions.

By moving provider.tf into each environment directory, the infrastructure becomes easier to manage, troubleshoot, and scale while maintaining a clean separation between environment-specific configuration and reusable Terraform code.

Understanding the Advanced Modular Approach: Root vs. Child Variables

In our advanced modular approach, we added variables.tf and terraform.tfvars inside our dev folder. Now, we have a variables.tf file in two different places.

Here is the simplest explanation of the difference between them:


📦 1. Child Module Variables (instance_module/variables.tf)

Think of this file as a "Requirement List" or an empty box.

  • What it does: It tells Terraform what inputs the module needs to create an instance (for example: Name, Memory, CPU).
  • Important Point: It contains NO hardcoded values. It only defines the structure. It waits for the values to come from the outside.

🏢 2. Root Module Variables (compute_service/dev/variables.tf)

Think of this file as a "Bridge" or a connector.

  • What it does: It sits in the dev folder and connects your actual environment values to the Child Module.
  • How it works: It takes the real data from your terraform.tfvars file and passes it down into the module block.

🔄 The Data Flow (How values move)

To make it fully clear, look at how a value like memory size flows through your files:

1. Child Module: instance_module/variables.tf

Here, you are simply telling Terraform that this module requires an input variable called env_memory_in_gbs to run. No fixed value (such as 12) is defined here. Instead, you only specify the variable's structure and data type, allowing the value to be provided later by the root module.

variable "env_memory_in_gbs" {

type = string

}

Root Module: compute_service/dev/variables.tf

Here, you declare a variable for the Dev environment so that Terraform can accept a value from an external source, such as terraform.tfvars. This variable acts as a placeholder that receives the environment-specific value, which can then be passed to the module.

variable "dev_memory" {

type = string

description = "Memory size dedicated for Dev environment"

}

3. terraform.tfvars (Inside the Dev Folder)

This is where you assign the actual value to the variable. Terraform automatically reads the value from terraform.tfvars and uses it during execution.

In other words, this file contains the environment-specific values that will be passed to the variables defined in the root module.

dev_memory = "12"

4. dev/instance.tf (Connecting the Variables)

In this file, we connect the value from terraform.tfvars to the module.

First, Terraform reads the value defined in terraform.tfvars and stores it in var.dev_memory. Then, instance.tf passes that value to the module variable. This allows the module to use the memory size specified for the Dev environment without hardcoding it inside the module.

module "dev_instance_creation" {

source = "/home/ngsoftlook/compute_service/instance_module"

# Module Variable = Root (Dev) Variable

env_memory_in_gbs = var.dev_memory

}

Conclusion

  • variables.tf inside the module: Defines the inputs required by the module. Think of it as an empty template that declares the values the module expects to receive.

  • variables.tf inside the Dev folder: Acts as a bridge between terraform.tfvars and the module. It accepts values from terraform.tfvars and passes them to the module.

Key Benefit of This Advanced Approach

With this approach, you no longer need to modify instance.tf or make changes inside the module whenever you want to update a configuration.

Instead, you can simply update the values in terraform.tfvars, and Terraform will automatically use the new settings during the next run.

This makes your code:

  • Easier to manage
  • More reusable
  • More scalable
  • Environment-specific without changing the module code

🎯 Key Takeaway Inside the Module (instance_module): Defines WHAT input is needed.

Inside the Environment (dev): Controls WHICH actual value goes into that input.

Thanks to this setup, if you want to change your server configuration in the future, you never need to touch the main code or the module folder. You only change the simple values inside terraform.tfvars!

Terraform Path Handling (~, ../, path.module)

Can Terraform Use ~ (Tilde)?

No. Terraform does not reliably expand ~ to the user's home directory like a shell does.

❌ Avoid:

private_key_path = "~/.oci/oci_api_key.pem"

Terraform may treat ~ as a literal character instead of converting it to the home directory path.


Using Relative Paths

Terraform supports standard relative paths such as:

private_key_path = "./keys/oci_api_key.pem"
private_key_path = "../keys/oci_api_key.pem"

Where:

  • ./ = Current directory
  • ../ = One directory above the current directory

Example

terraform/
├── keys/
│   └── oci_api_key.pem
└── dev/
    ├── provider.tf
    └── instance.tf

If provider.tf is inside the dev folder:

private_key_path = "../keys/oci_api_key.pem"

Terraform will look for:

terraform/keys/oci_api_key.pem

Best Practice: path.module

Use path.module to build file paths relative to the current Terraform module.

private_key_path = "${path.module}/keys/oci_api_key.pem"

Or:

private_key_path = "${path.module}/../keys/oci_api_key.pem"

How path.module Works

path.module returns the absolute path of the current module directory.

For example, if your Terraform module is located at:

/home/opc/terraform/dev

And you use:

private_key_path = "${path.module}/keys/oci_api_key.pem"

Terraform internally resolves it to:

/home/opc/terraform/dev/keys/oci_api_key.pem

Another example:

private_key_path = "${path.module}/../keys/oci_api_key.pem"

Terraform resolves it to:

/home/opc/terraform/keys/oci_api_key.pem

Benefits of path.module

  • Portable across environments
  • More reliable than hardcoded paths
  • Works consistently regardless of where Terraform is executed from
  • Recommended for reusable modules

Absolute Path Example

private_key_path = "/home/opc/.oci/oci_api_key.pem"

or

private_key_path = "/etc/oci/oci_api_key.pem"

Absolute paths are valid but may require changes when moving code between environments.


Recommendation

Method Recommendation
path.module ✅ Best Practice
Absolute Path ✅ Good
./ and ../ ⚠️ Supported but less portable
~ (tilde) ❌ Avoid

Interview Answer

Can Terraform use ~ in file paths?

Terraform does not reliably expand the tilde (~) like a shell. It is recommended to use absolute paths or Terraform path functions such as path.module.

Can Terraform use relative paths?

Yes. Terraform supports relative paths such as ./ and ../. However, path.module is generally preferred because it resolves paths consistently across different environments and makes modules more portable.

GitHub Issue: Folders Appearing as Empty Links After Push

Problem

I created a parent folder structure:

oci-terraform-code
├── compute_service
└── identity_service

After pushing the code to GitHub, both folders were visible in the repository, but clicking on them showed no files inside.

GitHub displayed:

compute_service
identity_service

However, the actual Terraform files and subfolders were not visible.


Root Cause

Both folders contained their own .git directories:

compute_service/.git
identity_service/.git

Because of this, Git treated them as separate Git repositories instead of normal folders.

Verification:

git ls-files

Output:

compute_service
identity_service

This indicated that Git was tracking the folders themselves rather than the files inside them.


Resolution

Step 1: Verify Nested Git Repositories

ls -la compute_service
ls -la identity_service

If .git exists inside either folder:

compute_service/.git
identity_service/.git

remove them.


Step 2: Remove Nested Git Metadata

rm -rf compute_service/.git
rm -rf identity_service/.git

Verify:

ls -la compute_service
ls -la identity_service

The .git folder should no longer be present.


Step 3: Remove Cached References

git rm --cached -r compute_service
git rm --cached -r identity_service

Step 4: Add Files Again

git add .

Step 5: Verify Files Are Being Tracked

git ls-files

Expected output:

compute_service/dev/provider.tf
compute_service/dev/instance.tf
compute_service/instance_module/main.tf

identity_service/dev/main.tf
identity_service/compartment_module/variables.tf

Step 6: Commit Changes

git commit -m "Added complete compute_service and identity_service code"

Step 7: Push to GitHub

git push origin master

Final Repository Structure

oci-terraform-code
├── compute_service
│   ├── dev
│   ├── test
│   ├── prod
│   ├── uat
│   └── instance_module
│
└── identity_service
    ├── dev
    ├── test
    ├── prod
    ├── uat
    └── compartment_module

Key Learning

When combining multiple Terraform projects into a single GitHub repository:

❌ Avoid keeping separate .git directories inside project folders.

compute_service/.git
identity_service/.git

✅ Maintain only one Git repository at the parent level.

oci-terraform-code
├── compute_service
└── identity_service

This ensures Git tracks all files and folders correctly and prevents folders from appearing as empty links on GitHub.

Adding New Terraform Service Folders to an Existing Git Repository

Now that the repository structure has been organized correctly, adding new Terraform service folders is very simple.

Current Repository Structure

oci-terraform-code
├── compute_service
├── identity_service

Example: Adding a New Service Folder

In the future, you may want to add additional Terraform services, such as:

oci-terraform-code
├── compute_service
├── identity_service
└── networking_service
    ├── dev
    ├── test
    ├── prod
    └── subnet_module

or

oci-terraform-code
├── compute_service
├── identity_service
└── app_subnet_service

Since the parent folder (oci-terraform-code) is already a Git repository, there is no need to run git init inside any new service folder.


Step 1: Create the New Folder

Navigate to the repository root:

cd ~/oci-terraform-code

Create a new service folder:

mkdir networking_service

Or copy an existing Terraform project:

cp -r subnet_service networking_service

Step 2: Check Repository Status

git status

Example output:

Untracked files:
  networking_service/

Step 3: Add Files to Git

Add all changes:

git add .

Or add only the new folder:

git add networking_service/

Step 4: Commit Changes

git commit -m "Added networking_service Terraform code"

Step 5: Push Changes to GitHub

git push origin master

Result

The new folder will automatically appear in GitHub:

oci-terraform-code
├── compute_service
├── identity_service
└── networking_service

Important Rule

❌ Do Not Do This

Never initialize Git inside a child service folder:

cd networking_service
git init

Do not create:

networking_service/.git

Nested Git repositories can cause GitHub to display folders incorrectly and may prevent files from being tracked properly.


✅ Correct Approach

Maintain only one Git repository at the parent folder level:

oci-terraform-code
│
├── .git
├── compute_service
├── identity_service
└── networking_service

Only the parent folder should contain the .git directory.


Recommended Enterprise-Style Repository Structure

As the project grows, additional Terraform services can be added using the same approach:

oci-terraform-code
├── compute_service
├── identity_service
├── networking_service
├── storage_service
├── security_service
├── monitoring_service
└── database_service

This structure is commonly used in Terraform projects, GitHub portfolios, and enterprise infrastructure repositories because it keeps services organized, scalable, and easy to manage.

Additional Note on Nested Git Repositories

⚠️ Avoid running git init inside individual service folders when the parent folder is already a Git repository.

For example:

cd networking_service
git init

or

cd storage_service
git init

Doing this creates an additional .git directory inside the service folder:

oci-terraform-code
├── .git
├── compute_service
├── identity_service
└── networking_service
    └── .git

This creates a nested Git repository. As a result, Git may track only the folder reference instead of the files inside it.

Possible symptoms include:

  • The folder appears on GitHub, but its contents are not visible.
  • The folder behaves like an empty link.
  • Terraform files inside the folder are not tracked correctly.
  • git ls-files may show only the folder name instead of the actual files.

To avoid this issue, maintain only one .git directory at the repository root level:

oci-terraform-code
├── .git
├── compute_service
├── identity_service
└── networking_service

This ensures all Terraform files are tracked and displayed correctly on GitHub.

⚠️ **GitHub.com Fallback** ⚠️