Skip to content

LAB 45 ‐ Create instance using modular approach

DevWithNehaCloud edited this page Jul 7, 2026 · 18 revisions

Infrastructure as Code (IaC) using Terraform: Multi-Environment Setup

This documentation explains the multi-environment cloud infrastructure architecture designed and deployed on Oracle Cloud Infrastructure (OCI) using a Modular Approach in Terraform.

We have provisioned four separate environments tailored to specific business and engineering needs:

  • Development (Dev): For initial feature implementation and coding.
  • Testing (Test/QA): For quality assurance and automated scripts.
  • User Acceptance Testing (UAT/Staging): Mirroring production for final client approvals.
  • Production (Prod): Live environment optimized for real-time application users.

📂 Project Directory Structure

To maintain clean code separation and reusability, we followed a structured directory pattern under the root folder compute_service. Each environment isolates its own Terraform state backend files, while leveraging a shared, reusable module for core compute resources.

Below is the directory tree of our setup:

compute_service/
├── instance_module/               # Reusable Architecture Core
│   ├── main.tf                    # Core OCI compute resource configurations
│   └── variables.tf               # Input parameter definitions (OCPU, Memory, etc.)
├── dev/                           # Development Environment
│   ├── instance.tf                # Calls instance_module with Dev variables
│   ├── provider.tf                # OCI provider & authentication setup
│   ├── privkey.pem               # Private key for SSH access
│   ├── terraform.tfstate          # Local state file for Dev
│   └── terraform.tfstate.backup
├── test/                          # Testing Environment
│   ├── instance.tf                # Calls instance_module with Test variables
│   ├── provider.tf                # OCI provider setup
│   ├── privkey.pem               # Private key for SSH access
│   ├── terraform.tfstate          # Local state file for Test
│   └── terraform.tfstate.backup
├── uat/                           # UAT Environment
│   ├── instance.tf                # Calls instance_module with UAT variables
│   ├── provider.tf                # OCI provider setup
│   ├── privkey.pem               # Private key for SSH access
│   ├── terraform.tfstate          # Local state file for UAT
│   └── terraform.tfstate.backup
└── prod/                          # Production Environment
    ├── instance.tf                # Calls instance_module with Prod variables
    ├── provider.tf                # OCI provider setup
    ├── privkey.pem               # Private key for SSH access
    ├── terraform.tfstate          # Local state file for Prod
    └── terraform.tfstate.backup

📸 Directory Structure Screenshot

image

Why Do We Keep Similar Files in Every Environment?

At first glance, it may appear that files such as provider.tf, instance.tf and even privkey.pem are duplicated across environments.

However, this separation is intentional and follows Terraform best practices.

Environment Isolation

Each environment should operate independently without impacting other environments.

For example:

  • Development (Dev) may be used for testing new changes.
  • Test may be used for validation and functional testing.
  • UAT may be used for user acceptance testing.
  • Production (Prod) hosts the live workload.

By maintaining separate directories, each environment has its own:

  • Provider configuration
  • State file
  • Private key
  • Module invocation
  • Resource lifecycle

This prevents accidental changes across environments.

Why Keep privkey.pem in Every Environment?

The private key does not necessarily have to be different for each environment.

For example, the same OCI API key may be used in all environments:

dev/privkey.pem
test/privkey.pem
uat/privkey.pem
prod/privkey.pem

The reason for storing the key inside each environment directory is not because the key must be unique, but because each environment is designed to be self-contained.

Benefits include:

  • Better organization
  • Easier portability
  • Environment isolation
  • Simpler provider configuration
  • Reduced risk of incorrect file references

Example:

provider "oci" {
  tenancy_ocid     = var.tenancy_ocid
  user_ocid        = var.user_ocid
  fingerprint      = var.fingerprint
  private_key_path = "./privkey.pem"
  region           = var.region
}

Since the provider uses a relative path, keeping the private key inside the same environment directory ensures that the configuration remains independent and easy to manage.

How Terraform Processes the Module

A common misconception is that Terraform executes only the files present inside the current environment directory.

For example, in the dev environment:

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

The instance.tf file contains a module block similar to:

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

  # variables
}

When the following command is executed:

cd dev
terraform apply

Terraform first reads all .tf files present in the dev directory:

  • provider.tf
  • instance.tf

Terraform then evaluates the module source specified inside instance.tf.

Since the module points to:

../instance_module

Terraform automatically loads and processes:

  • instance_module/main.tf
  • instance_module/variables.tf

Therefore, even though the command is executed from the dev directory, Terraform also reads and processes the files located inside the referenced module.

What Actually Happens During Execution?

The execution flow is:

terraform apply
        │
        ▼
Read dev/provider.tf
        │
        ▼
Read dev/instance.tf
        │
        ▼
Locate module source
(../instance_module)
        │
        ▼
Load main.tf
Load variables.tf

        │
        ▼
Create Execution Plan
        │
        ▼
Provision Resources

FOR DEV ENVIRONMENT

provider "oci" {
  auth             = "APIKey"
  tenancy_ocid     = "ocid1.tenancy.oc1..aaaaaaaacxrxza63ajbfdhvukzuxpv4y3oksxh256migpqx6lazpbbejuuga"
  user_ocid        = "ocid1.user.oc1..aaaaaaaa5m4rn7uj5exypqxsz54tmkqctcuzd7o4ehy2f35flcdzznncw77a"
  private_key_path = "/home/ngsoftlook/compute_service/dev/privkey.pem"
  fingerprint      = "4c:bb:0e:8b:6e:b1:df:51:59:bf:c6:e3:60:2c:d6:29"
  region           = "us-ashburn-1"
}
image
module "dev_instance_creation" {
  source                = "/home/ngsoftlook/compute_service/instance_module"
  env_Swiggy_Finance    = "dev"
  env_display_name      = "dev_instance"
  env_memory_in_gbs     = "12"
  env_ocpus             = "1"
  env_boot_volume_size  = 50
}
image image image

For the TEST, UAT, and PROD environments, we only need to update the values in the instance.tf file according to the specific requirements of each environment.

for test (instance.tf)

image

for uat (instance.tf)

image

for prod (instance.tf)

image

This means Terraform does not create resources directly from instance.tf.

Instead:

  1. instance.tf calls the module.
  2. Terraform loads the module files.
  3. Resources defined in main.tf are evaluated.
  4. Variables defined in variables.tf are processed.
  5. Resources are created according to the module configuration.

Benefits of This Structure

  • Clear separation between environments
  • Reusable infrastructure code through modules
  • Independent state management
  • Easier maintenance and troubleshooting
  • Reduced code duplication
  • Consistent deployments across environments
  • Better scalability for large Terraform projects

Conclusion

Although each environment contains only a small number of files such as provider.tf, instance.tf, and privkey.pem, Terraform is still able to create resources because instance.tf references the shared module.

When Terraform executes from an environment directory, it reads the local configuration files first and then automatically loads all Terraform files inside the referenced module. This approach provides environment isolation while still allowing infrastructure code to be reused across Dev, Test, UAT, and Production environments.

⚙️ Environment Configuration Strategy

We have differentiated the environments by varying their Compute Shapes (OCPU & Memory) and Boot Volume Sizes inside each environment's instance.tf parameters. This balance optimizes cloud expenditure for testing while guaranteeing high availability and performance in Production.

Environment OCPU Count Memory (GB) Boot Volume Size Purpose
Dev 1 12 GB 50 GB Lightweight, active development & debugging.
Test 2 16 GB 50 GB Standard QA cycles and automated unit testing.
UAT 2 32 GB 100 GB High-capacity staging for client reviews.
Prod 4 64 GB 150 GB Live deployment environment for production workloads.

🚀 Step-by-Step Execution Workflow

To deploy or update any specific environment, the process requires navigating into that environment's distinct folder before running Terraform commands. This safely isolates the terraform.tfstate logic.

Step 1: Initialize the Working Directory

Navigate to the desired environment folder (e.g., cd dev or test or uat or prod) and run initialize to fetch provider plugins and parse the shared module references.

terraform init

Step 2: Validate and Plan the Infrastructure Generate an execution plan to verify what resources will be created or altered according to that environment's customized sizing parameters.

terraform plan

Step 3: Apply the Configuration Execute the changes to provision the OCI compute instance.

terraform apply 
image

Now, you can see that the resource address is displayed with the module path instead of the direct resource name. This indicates that the compute instance is being managed through the Terraform module. The output will show a resource address similar to:

module.dev_instance_creation.oci_core_instance.generated_oci_core_instance

This confirms that Terraform is creating and managing the instance using the modular approach.

🔍 Verification & Artifacts After successful deployment, each environment successfully maintains its unique cloud footprint tracked separately via local backend state configurations (terraform.tfstate).

📸 OCI Console Dashboards

image

Dev Instances View:

image image

Test Instances View:

image image

UAT Instances View:

image image

Prod Instances View:

image image

View the Terraform State

To view all resources currently managed by Terraform, run the following command:

terraform state list

You will notice that the resource address now includes the module path, indicating that the resource is being managed through the Terraform module rather than being defined directly in the root configuration.

image image image image

Destroy Individual Instances Using the -target Option

Since we created four separate instances (dev, test, uat, and prod) using individual modules, we can selectively destroy them one at a time by using Terraform's -target option. This allows us to remove a specific instance without affecting the other resources managed by Terraform.

In the following steps, we will destroy each instance individually and verify the changes before proceeding to the next one.

Destroy the DEV Instance

terraform destroy -target="module.dev_instance_creation"
image image

Destroy the TEST Instance

terraform destroy -target="module.test_instance_creation"
image

Destroy the UAT Instance

terraform destroy -target="module.uat_instance_creation"
image

Destroy the PROD Instance

terraform destroy -target="module.prod_instance_creation"
image

Now check in console

image

Note: The -target option allows Terraform to destroy only the specified module and its associated resources, leaving all other resources unchanged.

Clone this wiki locally