{"id":152,"date":"2026-01-04T12:00:00","date_gmt":"2026-01-04T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=152"},"modified":"2026-10-30T07:00:43","modified_gmt":"2026-10-30T07:00:43","slug":"infrastructure-as-code-devops-automated-multi-environment-deployment-with-aws-codepipeline","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=152","title":{"rendered":"Infrastructure as Code DevOps: Automated Multi-Environment Deployment with AWS CodePipeline"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">After building and manually deploying infrastructure, I faced the real DevOps question:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;How do I deploy infrastructure changes safely across multiple environments without manual terraform apply commands?&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is where Infrastructure as Code meets DevOps automated, repeatable, multi-environment deployments with AWS CodePipeline.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What I Built<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A complete CI\/CD pipeline that automatically deploys Terraform infrastructure to Dev and Staging environments with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Automated deployments on git push<\/li>\n\n\n\n<li>Separate state management per environment<\/li>\n\n\n\n<li>Environment-specific configurations<\/li>\n\n\n\n<li>Remote state in S3 with DynamoDB locking<\/li>\n\n\n\n<li>Secrets managed via AWS Systems Manager Parameter Store<\/li>\n\n\n\n<li>CodeBuild for terraform execution<\/li>\n\n\n\n<li>Multi-environment support (dev, staging, production-ready)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">CI\/CD Pipeline Architecture<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Developer \u2192 Git Push \u2192 GitHub\n                          \u2193\n             AWS CodePipeline (triggered)\n                          \u2193\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502                                   \u2502\n   Dev Branch                          Staging Branch\n        \u2502                                   \u2502\n        \u2193                                   \u2193\n   CodeBuild (Dev)                    CodeBuild (Staging)\n        \u2502                                   \u2502\n   buildspec-dev.yml                   buildspec-stag.yml\n        \u2502                                   \u2502\n   terraform init                      terraform init\n   -backend-config=dev.conf            -backend-config=stag.conf\n        \u2502                                   \u2502\n   terraform apply                     terraform apply\n   -var-file=dev.tfvars                -var-file=stag.tfvars\n        \u2502                                   \u2502\n        \u2193                                   \u2193\n   S3 State (Dev)                      S3 State (Staging)\n   DynamoDB Lock (Dev)                 DynamoDB Lock (Staging)\n        \u2502                                   \u2502\n        \u2193                                   \u2193\n   AWS Infrastructure (Dev)            AWS Infrastructure (Staging)\n   devdemo5.rezaops.com                stagedemo5.rezaops.com<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Multi-Environment Configuration Strategy<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Environment Separation with Backend Configs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the key pattern &#8211; separate backend configs per environment:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">dev.conf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>bucket = \"terraform-on-aws-for-ec2-lowyi\"\nkey = \"iacdevops\/dev\/terraform.tfstate\"\nregion = \"us-east-1\" \ndynamodb_table = \"iacdevops-dev-tfstate\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">stag.conf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>bucket = \"terraform-on-aws-for-ec2-lowyi\"\nkey = \"iacdevops\/stag\/terraform.tfstate\"\nregion = \"us-east-1\" \ndynamodb_table = \"iacdevops-stag-tfstate\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Why separate state files?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Without separation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Single state file: terraform.tfstate\nDev changes \u2192 Applies to production \u2717\nStaging changes \u2192 Overwrites dev state \u2717\nNo rollback per environment \u2717<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With separation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Dev state: iacdevops\/dev\/terraform.tfstate\nStaging state: iacdevops\/stag\/terraform.tfstate\nProduction state: iacdevops\/prod\/terraform.tfstate\n\nDev changes \u2192 Only affects dev \u2713\nStaging is independent \u2713\nCan rollback each environment separately \u2713<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DynamoDB for state locking:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dynamodb_table = \"iacdevops-dev-tfstate\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What happens without locking:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User A runs: terraform apply\nUser B runs: terraform apply (simultaneously)\n\nBoth read same state file\nBoth make conflicting changes\nState file corrupted \u2717<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With DynamoDB locking:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User A runs: terraform apply\n  \u2192 Acquires lock in DynamoDB\n  \nUser B runs: terraform apply\n  \u2192 Waits for lock: \"State locked by user A\"\n  \nUser A completes \u2192 Releases lock\nUser B acquires lock \u2192 Applies safely<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DynamoDB table structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Table: iacdevops-dev-tfstate\nPrimary Key: LockID\n\nItem when locked:\n{\n  \"LockID\": \"terraform-on-aws-for-ec2-lowyi\/iacdevops\/dev\/terraform.tfstate-md5\",\n  \"Info\": \"user@hostname running terraform apply\",\n  \"Digest\": \"abc123...\",\n  \"Created\": \"2025-01-15T10:30:00Z\"\n}<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Environment-Specific Variables<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">dev.tfvars:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>environment = \"dev\"\n\nvpc_availability_zones = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\nvpc_public_subnets = [\"10.0.101.0\/24\", \"10.0.102.0\/24\", \"10.0.103.0\/24\"]\n\ninstance_type = \"t2.micro\"\ndns_name = \"devdemo5.rezaops.com\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">stag.tfvars:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>environment = \"stag\"\n\nvpc_availability_zones = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\nvpc_public_subnets = [\"10.0.101.0\/24\", \"10.0.102.0\/24\", \"10.0.103.0\/24\"]\n\ninstance_type = \"t2.micro\"  # Could be t3.small for staging\ndns_name = \"stagedemo5.rezaops.com\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">How environment variable affects everything:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Local values (c3-local-values.tf)\nlocals {\n  environment = var.environment\n  name = \"${var.business_divsion}-${var.environment}\"\n  # name = \"hr-dev\" or \"hr-stag\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This prefix propagates everywhere:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Resource names:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># VPC\nname = \"${local.name}-${var.vpc_name}\"\n# Result: \"hr-dev-myvpc\" or \"hr-stag-myvpc\"\n\n# ALB\nname = \"${local.name}-alb\"\n# Result: \"hr-dev-alb\" or \"hr-stag-alb\"\n\n# Security Groups\nname = \"${local.name}-public-bastion-sg\"\n# Result: \"hr-dev-public-bastion-sg\"\n\n# Launch Template\nname = \"${local.name}\"\n# Result: \"hr-dev\" or \"hr-stag\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Benefits:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Easy identification:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aws ec2 describe-vpcs --filters \"Name=tag:environment,Values=dev\"\n# Shows only dev VPCs<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No naming conflicts:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Dev VPC: hr-dev-myvpc\nStaging VPC: hr-stag-myvpc\nBoth exist simultaneously \u2713<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Resource tagging:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>common_tags = {\n  owners = \"hr\"\n  environment = \"dev\"  # or \"stag\"\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">AWS CodeBuild Buildspec Files<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Dev Environment Buildspec<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">buildspec-dev.yml:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>version: 0.2\n\nenv:\n  variables:\n    TERRAFORM_VERSION: \"1.9.0\"\n    TF_COMMAND: \"apply\"\n    #TF_COMMAND: \"destroy\"\n  parameter-store:\n    AWS_ACCESS_KEY_ID: \"\/CodeBuild\/MY_AWS_ACCESS_KEY_ID\"\n    AWS_SECRET_ACCESS_KEY: \"\/CodeBuild\/MY_AWS_SECRET_ACCESS_KEY\"\n\nphases:\n  install:\n    runtime-versions:\n      python: 3.11\n    on-failure: ABORT\n    commands:\n      - tf_version=$TERRAFORM_VERSION\n      - wget https:\/\/releases.hashicorp.com\/terraform\/\"$TERRAFORM_VERSION\"\/terraform_\"$TERRAFORM_VERSION\"_linux_amd64.zip\n      - unzip terraform_\"$TERRAFORM_VERSION\"_linux_amd64.zip\n      - mv terraform \/usr\/local\/bin\/\n      \n  pre_build:\n    on-failure: ABORT\n    commands:\n      - echo terraform execution started on `date`\n      \n  build:\n    on-failure: ABORT\n    commands:\n      - cd \"$CODEBUILD_SRC_DIR\/terraform-manifests\"\n      - ls -lrt \"$CODEBUILD_SRC_DIR\/terraform-manifests\"\n      - terraform --version\n      - terraform init -input=false --backend-config=dev.conf\n      - terraform validate\n      - terraform plan -lock=false -input=false -var-file=dev.tfvars\n      - terraform $TF_COMMAND -input=false -var-file=dev.tfvars -auto-approve\n      \n  post_build:\n    on-failure: CONTINUE\n    commands:\n      - echo terraform execution completed on `date`<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Buildspec Phases Explained<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Phase 1: Install<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>install:\n  runtime-versions:\n    python: 3.11\n  commands:\n    - wget https:\/\/releases.hashicorp.com\/terraform\/1.9.0\/terraform_1.9.0_linux_amd64.zip\n    - unzip terraform_1.9.0_linux_amd64.zip\n    - mv terraform \/usr\/local\/bin\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What happens:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>CodeBuild starts Ubuntu container<\/li>\n\n\n\n<li>Downloads Terraform binary from HashiCorp<\/li>\n\n\n\n<li>Extracts and moves to PATH<\/li>\n\n\n\n<li>Makes Terraform available globally<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Why not use pre-installed Terraform?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CodeBuild may have:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Outdated Terraform version<\/li>\n\n\n\n<li>No Terraform at all<\/li>\n\n\n\n<li>Wrong version for your code<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Downloading specific version:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Guarantees consistent version<\/li>\n\n\n\n<li>Works with your Terraform code<\/li>\n\n\n\n<li>Prevents version mismatch issues<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Phase 2: Pre-build<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pre_build:\n  commands:\n    - echo terraform execution started on `date`<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Simple logging for CloudWatch. Could add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>- aws sts get-caller-identity  # Verify credentials\n- terraform --version  # Log Terraform version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Phase 3: Build (Main Phase)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>build:\n  commands:\n    - cd \"$CODEBUILD_SRC_DIR\/terraform-manifests\"\n    - terraform init -input=false --backend-config=dev.conf\n    - terraform validate\n    - terraform plan -lock=false -input=false -var-file=dev.tfvars\n    - terraform $TF_COMMAND -input=false -var-file=dev.tfvars -auto-approve<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">$CODEBUILD_SRC_DIR explained:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CODEBUILD_SRC_DIR=\/codebuild\/output\/src123456789\/src<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">CodeBuild clones your git repo here. We need to navigate to terraform-manifests folder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">terraform init -input=false &#8211;backend-config=dev.conf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># In c1-versions.tf:\nbackend \"s3\" {}  # Empty! No hardcoded values<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Backend config provided at runtime:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform init --backend-config=dev.conf<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This pattern allows:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Same Terraform code for all environments<\/li>\n\n\n\n<li>Backend configuration determined by buildspec<\/li>\n\n\n\n<li>No environment-specific code branches<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">-lock=false in terraform plan:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform plan -lock=false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">plan doesn&#8217;t modify state, so locking is optional. Skipping lock:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Allows parallel plan operations<\/li>\n\n\n\n<li>Prevents unnecessary DynamoDB writes<\/li>\n\n\n\n<li>Speeds up pipeline<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Important: apply ALWAYS uses locking (can&#8217;t disable).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-auto-approve:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform apply -auto-approve<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Without -auto-approve:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Do you want to perform these actions?\n  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: _<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">CodeBuild can&#8217;t interactively respond! Build would hang forever.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Phase 4: Post-build<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>post_build:\n  on-failure: CONTINUE  # Don't fail build if this fails\n  commands:\n    - echo terraform execution completed on `date`<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">on-failure: CONTINUE means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If post_build fails, mark build as succeeded anyway<\/li>\n\n\n\n<li>Useful for non-critical cleanup or notifications<\/li>\n\n\n\n<li>Logging should never fail the build<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\">Environment Variables and Secrets<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Variables in buildspec:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>env:\n  variables:\n    TERRAFORM_VERSION: \"1.9.0\"\n    TF_COMMAND: \"apply\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These are plain text, visible in:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Buildspec file<\/li>\n\n\n\n<li>CodeBuild logs<\/li>\n\n\n\n<li>Build history<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Secrets from Parameter Store:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>env:\n  parameter-store:\n    AWS_ACCESS_KEY_ID: \"\/CodeBuild\/MY_AWS_ACCESS_KEY_ID\"\n    AWS_SECRET_ACCESS_KEY: \"\/CodeBuild\/MY_AWS_SECRET_ACCESS_KEY\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">How Parameter Store secrets work:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Store secrets:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aws ssm put-parameter \\\n  --name \"\/CodeBuild\/MY_AWS_ACCESS_KEY_ID\" \\\n  --value \"AKIAIOSFODNN7EXAMPLE\" \\\n  --type \"SecureString\"\n\naws ssm put-parameter \\\n  --name \"\/CodeBuild\/MY_AWS_SECRET_ACCESS_KEY\" \\\n  --value \"wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY\" \\\n  --type \"SecureString\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">CodeBuild fetches at runtime:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Build starts \u2192 CodeBuild IAM role \u2192 Parameter Store\n            \u2192 Decrypt SecureString \u2192 Inject as environment variable<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Available in build:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>echo $AWS_ACCESS_KEY_ID\n# AKIAIOSFODNN7EXAMPLE\n\nterraform plan  # Uses credentials automatically<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Benefits:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u2713 Credentials not in code \u2713 Not visible in logs (masked) \u2713 Rotatable without code changes \u2713 IAM-controlled access \u2713 Audit trail in CloudTrail<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alternative: Secrets Manager<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>env:\n  secrets-manager:\n    AWS_ACCESS_KEY_ID: \"my-secret:access_key_id\"\n    AWS_SECRET_ACCESS_KEY: \"my-secret:secret_access_key\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Parameter Store vs Secrets Manager:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Parameter Store:\n- Free (up to 10,000 parameters)\n- Simple key-value\n- Good for config values\n\nSecrets Manager:\n- $0.40 per secret per month\n- Automatic rotation support\n- Better for database passwords, API keys<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Terraform Backend Configuration<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Backend Declaration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">c1-versions.tf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_providers {\n    aws = {\n      source = \"hashicorp\/aws\"\n      version = \"5.70.0\"\n    }\n  }\n  \n  backend \"s3\" {}  # Empty backend block!\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Why empty backend?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Traditional approach (hardcoded):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>backend \"s3\" {\n  bucket = \"my-terraform-state\"\n  key = \"dev\/terraform.tfstate\"\n  region = \"us-east-1\"\n  dynamodb_table = \"terraform-state-lock\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Problems:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Dev and staging need different configurations<\/li>\n\n\n\n<li>Must maintain separate code branches<\/li>\n\n\n\n<li>Can&#8217;t use same code for multiple environments<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Dynamic backend configuration:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Dev environment\nterraform init -backend-config=dev.conf\n\n# Staging environment\nterraform init -backend-config=stag.conf -reconfigure<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">-reconfigure flag:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform init -backend-config=stag.conf -reconfigure<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What -reconfigure does:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Without -reconfigure:\n  Backend already configured for dev\n  Error: Backend configuration changed\n  \nWith -reconfigure:\n  Detected configuration change\n  Migrating state from dev backend to staging backend\n  Asking: Do you want to copy state? (must type 'yes')\n  \nIn CodeBuild with -reconfigure:\n  Skips migration questions\n  Configures new backend\n  No interactive prompts \u2713<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">State File Organization in S3<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>s3:\/\/terraform-on-aws-for-ec2-lowyi\/\n\u251c\u2500\u2500 iacdevops\/\n\u2502   \u251c\u2500\u2500 dev\/\n\u2502   \u2502   \u2514\u2500\u2500 terraform.tfstate       # Dev environment state\n\u2502   \u2502   \u2514\u2500\u2500 terraform.tfstate.backup\n\u2502   \u251c\u2500\u2500 stag\/\n\u2502   \u2502   \u2514\u2500\u2500 terraform.tfstate       # Staging environment state\n\u2502   \u2502   \u2514\u2500\u2500 terraform.tfstate.backup\n\u2502   \u2514\u2500\u2500 prod\/\n\u2502       \u2514\u2500\u2500 terraform.tfstate       # Production environment state (future)\n\u2502       \u2514\u2500\u2500 terraform.tfstate.backup<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">State file versions:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">S3 versioning MUST be enabled:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aws s3api put-bucket-versioning \\\n  --bucket terraform-on-aws-for-ec2-lowyi \\\n  --versioning-configuration Status=Enabled<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Why versioning matters:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Scenario: Accidental terraform destroy<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Without versioning:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  terraform destroy -auto-approve\n  State file deleted\n  No record of infrastructure \u2717\n  Must rebuild from scratch \u2717<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With versioning:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  terraform destroy -auto-approve\n  State file deleted \u2192 S3 creates delete marker\n  Previous versions preserved\n  Can restore previous state:\n    aws s3api list-object-versions --bucket ... --prefix ...\n    aws s3api get-object --version-id ... \n  Infrastructure recoverable \u2713<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">AWS CodePipeline Setup<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Pipeline Stages<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Stage 1: Source\n  \u2193\n  GitHub repository (on push)\n  \u2193\nStage 2: Build (Dev)\n  \u2193\n  CodeBuild project (Dev)\n  \u2193\n  Buildspec: buildspec-dev.yml\n  \u2193\nStage 3: Build (Staging)\n  \u2193\n  CodeBuild project (Staging)\n  \u2193\n  Buildspec: buildspec-stag.yml<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Pipeline Triggers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Automatic trigger on git push:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Developer workflow\ngit add terraform-manifests\/c10-02-ALB-application-loadbalancer.tf\ngit commit -m \"Updated ALB listener rules\"\ngit push origin main\n\n# Pipeline triggered automatically:\n  1. Source stage: Downloads latest code\n  2. Dev build: Deploys to dev environment\n  3. Staging build: Deploys to staging environment<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Manual approval (optional):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Stage 1: Source\n  \u2193\nStage 2: Build (Dev) - Automatic\n  \u2193\nStage 3: Manual Approval\n  \u2193\nStage 4: Build (Staging) - After approval\n  \u2193\nStage 5: Manual Approval\n  \u2193\nStage 6: Build (Production) - After approval<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">CodeBuild IAM Permissions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Required IAM policy for CodeBuild:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"ec2:*\",\n        \"elasticloadbalancing:*\",\n        \"autoscaling:*\",\n        \"route53:*\",\n        \"acm:*\",\n        \"sns:*\",\n        \"s3:*\"\n      ],\n      \"Resource\": \"*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"s3:GetObject\",\n        \"s3:PutObject\"\n      ],\n      \"Resource\": \"arn:aws:s3:::terraform-on-aws-for-ec2-lowyi\/*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"dynamodb:GetItem\",\n        \"dynamodb:PutItem\",\n        \"dynamodb:DeleteItem\"\n      ],\n      \"Resource\": \"arn:aws:dynamodb:*:*:table\/iacdevops-*-tfstate\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"ssm:GetParameter\",\n        \"ssm:GetParameters\"\n      ],\n      \"Resource\": \"arn:aws:ssm:*:*:parameter\/CodeBuild\/*\"\n    }\n  ]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Why these permissions?<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>ec2:* &#8211; Create VPCs, instances, security groups<\/li>\n\n\n\n<li>elasticloadbalancing:* &#8211; Create ALBs, target groups<\/li>\n\n\n\n<li>autoscaling:* &#8211; Create launch templates, ASGs<\/li>\n\n\n\n<li>route53:* &#8211; Create DNS records<\/li>\n\n\n\n<li>acm:* &#8211; Request and validate certificates<\/li>\n\n\n\n<li>s3:* (state bucket) &#8211; Read\/write state files<\/li>\n\n\n\n<li>dynamodb:* (lock table) &#8211; State locking<\/li>\n\n\n\n<li>ssm:GetParameter &#8211; Fetch secrets<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Principle of Least Privilege:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production, restrict further:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"Effect\": \"Allow\",\n  \"Action\": \"ec2:*\",\n  \"Resource\": \"*\",\n  \"Condition\": {\n    \"StringEquals\": {\n      \"ec2:ResourceTag\/Environment\": \"dev\"\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This limits EC2 actions to resources tagged with Environment=dev.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Deploying Infrastructure via Pipeline<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Deployment Workflow<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>1. Developer makes change:\n   \u251c\u2500 Modify: c10-02-ALB-application-loadbalancer.tf\n   \u2514\u2500 Change: Add new listener rule\n\n2. Commit and push:\n   git add .\n   git commit -m \"Add \/api path routing\"\n   git push origin main\n\n3. CodePipeline triggered automatically:\n   \u251c\u2500 Source stage: Downloads code\n   \u2514\u2500 Status: Source Complete\n\n4. Dev Build (CodeBuild):\n   \u251c\u2500 Start time: 14:30:00\n   \u251c\u2500 Install: Download Terraform \n   \u251c\u2500 Pre-build: Log start time\n   \u251c\u2500 Build:\n   \u2502  \u251c\u2500 terraform init -backend-config=dev.conf\n   \u2502  \u2502  Backend initialized: s3:\/\/...\/ iacdevops\/dev\/terraform.tfstate\n   \u2502  \u251c\u2500 terraform validate\n   \u2502  \u2502  Success! Configuration is valid.\n   \u2502  \u251c\u2500 terraform plan -var-file=dev.tfvars\n   \u2502  \u2502  Plan: 1 to add, 2 to change, 0 to destroy.\n   \u2502  \u2514\u2500 terraform apply -var-file=dev.tfvars -auto-approve\n   \u2502     Apply complete! Resources: 1 added, 2 changed, 0 destroyed.\n   \u251c\u2500 Post-build: Log completion time\n   \u251c\u2500 Duration: 3 minutes 45 seconds\n   \u2514\u2500 Status: Build Complete\n\n5. Staging Build (CodeBuild):\n   \u251c\u2500 Start time: 14:35:00\n   \u251c\u2500 [Same steps as dev build]\n   \u251c\u2500 backend-config: stag.conf\n   \u251c\u2500 var-file: stag.tfvars\n   \u251c\u2500 Duration: 3 minutes 50 seconds\n   \u2514\u2500 Status: Build Complete\n\n6. Total Pipeline Duration: ~8 minutes\n\n7. Verification:\n   Dev: https:\/\/devdemo5.rezaops.com\n   Staging: https:\/\/stagedemo5.rezaops.com\n   Both updated with new listener rule \u2713<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Viewing Build Logs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">CloudWatch Logs:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log Group: \/aws\/codebuild\/terraform-dev-build<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log Stream: abc123-def456-789012<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Container] 2025\/01\/15 14:30:15 Running command terraform init -backend-config=dev.conf\nInitializing the backend...\n\nSuccessfully configured the backend \"s3\"! Terraform will automatically\nuse this backend unless the backend configuration changes.\n\n[Container] 2025\/01\/15 14:30:45 Running command terraform validate\nSuccess! The configuration is valid.\n\n[Container] 2025\/01\/15 14:31:00 Running command terraform plan -var-file=dev.tfvars\n...\nPlan: 1 to add, 2 to change, 0 to destroy.\n...\n\n[Container] 2025\/01\/15 14:31:30 Running command terraform apply -var-file=dev.tfvars -auto-approve\nmodule.alb.aws_lb_listener_rule.myapp1-rule: Modifying...\nmodule.alb.aws_lb_listener_rule.myapp1-rule: Modifications complete\nApply complete! Resources: 1 added, 2 changed, 0 destroyed.<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Destroying Infrastructure via Pipeline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To destroy environment:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Update buildspec:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>env:\n  variables:\n    TF_COMMAND: \"destroy\"  # Changed from \"apply\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Commit and push:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git add buildspec-dev.yml\ngit commit -m \"Destroy dev environment\"\ngit push origin main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline executes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform destroy -input=false -var-file=dev.tfvars -auto-approve<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Destroy complete! Resources: 45 destroyed.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Warning for production:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production, never use auto-approve for destroy!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Better approach:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Add manual approval before destroy\n- TF_COMMAND: \"destroy\"\n- Require manual confirmation in pipeline\n- Send SNS notification before destroy<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Real-World Scenarios<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 1: Update ALB Listener Configuration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change required: Add new health check path<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">File: c10-02-ALB-application-loadbalancer.tf<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Before:\nhealth_check = {\n  path = \"\/app1\/index.html\"\n  interval = 30\n}\n\n# After:\nhealth_check = {\n  path = \"\/health\"  # New health check endpoint\n  interval = 15      # Faster checks\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Deployment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git add c10-02-ALB-application-loadbalancer.tf\ngit commit -m \"Update ALB health check path and interval\"\ngit push origin main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline execution:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>14:00:00 - Source stage triggered\n14:00:30 - Dev build started\n14:01:15 - terraform plan output:\n  module.alb.aws_lb_target_group.mytg1: will be updated in-place\n  \n14:02:00 - terraform apply:\n  module.alb.aws_lb_target_group.mytg1: Modifying...\n  module.alb.aws_lb_target_group.mytg1: Modifications complete\n  \n14:02:45 - Dev build complete\n14:03:00 - Staging build started\n14:05:45 - Staging build complete\n\nTotal time: 5 minutes 45 seconds<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verification:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Check dev environment\naws elbv2 describe-target-groups \\\n  --names hr-dev-mytg1 \\\n  --query 'TargetGroups[0].HealthCheckPath'\n# Output: \"\/health\"\n\n# Check staging environment\naws elbv2 describe-target-groups \\\n  --names hr-stag-mytg1 \\\n  --query 'TargetGroups[0].HealthCheckPath'\n# Output: \"\/health\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 2: Scale Up Auto Scaling Group<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change required: Increase ASG capacity for load testing<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">File: dev.tfvars<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Before:\ndesired_capacity = 2\nmax_size = 5\n\n# After:\ndesired_capacity = 5  # Scale up for load test\nmax_size = 10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Deployment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git add terraform-manifests\/dev.tfvars\ngit commit -m \"Scale up dev ASG for load testing\"\ngit push origin main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What happens:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform plan output:\n  # aws_autoscaling_group.my_asg will be updated in-place\n  ~ resource \"aws_autoscaling_group\" \"my_asg\" {\n      ~ desired_capacity = 2 -&gt; 5\n      ~ max_size         = 5 -&gt; 10\n    }\n\nterraform apply:\n  aws_autoscaling_group.my_asg: Modifying...\n  aws_autoscaling_group.my_asg: Still modifying... [10s elapsed]\n  \nASG launches 3 new instances:\n  Instance 1: i-0abc123 launching...\n  Instance 2: i-0def456 launching...\n  Instance 3: i-0ghi789 launching...\n  \nHealth checks:\n  All instances healthy after 2 minutes\n  \nLoad balancer:\n  Target group now has 5 healthy targets<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Impact: Only dev environment scaled, staging unchanged!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 3: Pipeline Failure &#8211; Terraform Validation Error<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bad change committed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># c13-06-autoscaling-ttsp.tf\nresource \"aws_autoscaling_policy\" \"avg_cpu_policy\" {\n  name = \"cpu-policy\"\n  target_value = \"invalid\"  # Should be number, not string!\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline execution:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Source stage: \u2713 Complete\nDev build: Started\n  terraform validate:\n    Error: Invalid value for target_value\n    Expected number, got string\n  \n  Build status: FAILED\n  Duration: 45 seconds<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Staging build: Never runs! (Dev failed)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Result:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Dev environment unchanged (apply never ran)<\/li>\n\n\n\n<li>Staging environment unchanged<\/li>\n\n\n\n<li>Infrastructure protected from bad configuration<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Fix:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>target_value = 50.0  # Correct: number \ngit add c13-06-autoscaling-ttsp.tf\ngit commit -m \"Fix: target_value should be number\"\ngit push origin main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline re-runs successfully!<\/p>\n\n\n<h3 class=\"wp-block-heading\">Lessons Learned<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">1. State Locking Failed &#8211; DynamoDB Table Missing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Error:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Error acquiring state lock\nError: Error locking state: ResourceNotFoundException<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cause: DynamoDB table doesn&#8217;t exist!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Solution:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aws dynamodb create-table \\\n  --table-name iacdevops-dev-tfstate \\\n  --attribute-definitions AttributeName=LockID,AttributeType=S \\\n  --key-schema AttributeName=LockID,KeyType=HASH \\\n  --billing-mode PAY_PER_REQUEST<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. CodeBuild Can&#8217;t Access Parameter Store<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Error:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Error retrieving value from parameter store\nAccessDeniedException: User is not authorized<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cause: CodeBuild IAM role missing SSM permissions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Solution: Add to CodeBuild service role:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"Effect\": \"Allow\",\n  \"Action\": \"ssm:GetParameter\",\n  \"Resource\": \"arn:aws:ssm:*:*:parameter\/CodeBuild\/*\"\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Terraform Version Mismatch<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Error:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Error: state file format version 4 is too new\nThis version of Terraform requires state file version 3<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cause: State created with Terraform 1.0, buildspec uses 1.9.0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Solution: Update buildspec:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TERRAFORM_VERSION: \"1.0.0\"  # Match state file version<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4. Backend Initialization in Wrong Directory<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Error:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform init\nError: No configuration files<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cause:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Missing this:\ncd \"$CODEBUILD_SRC_DIR\/terraform-manifests\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform ran in root directory, no .tf files found!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. ACM Certificate Validation Timeout in Pipeline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Problem: Pipeline takes 20+ minutes, times out waiting for ACM validation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cause: ACM DNS validation requires Route53 records, takes 5-10 minutes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Solution 1: Increase CodeBuild timeout:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># In CodePipeline build stage:\ntimeout_in_minutes: 30<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Solution 2: Pre-create and reuse ACM certificate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Instead of creating new cert:\ndata \"aws_acm_certificate\" \"existing\" {\n  domain = \"*.rezaops.com\"\n}\n\n# Use existing cert:\ncertificate_arn = data.aws_acm_certificate.existing.arn<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6. Auto-Approve Destroyed Wrong Environment<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Horror story:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Updated wrong buildspec:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># In buildspec-PROD.yml (!!!)\nTF_COMMAND: \"destroy\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pushed to main \u2192 Pipeline destroyed production!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Prevention:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Never auto-approve destroy in production<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use branch protection:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>main branch:\n  - Requires pull request\n  - Requires approvals: 2\n  - No direct push<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add confirmation comment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TF_COMMAND: \"apply\"\n# To destroy, MUST manually edit this to \"destroy\" AND commit message MUST contain \"DESTROY-CONFIRMED\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Separate pipelines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dev-pipeline: Auto-deploys on push to dev branch\nstag-pipeline: Auto-deploys on push to staging branch  \nprod-pipeline: Manual approval required, no auto-destroy<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">7. Concurrent Builds Caused State Lock Conflict<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Problem: Pushed 2 commits quickly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Commit 1: Update ALB \u2192 Pipeline starts\nCommit 2: Update ASG \u2192 Pipeline starts immediately<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both tried to acquire state lock:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Build 1: Acquired lock at 14:00:00\nBuild 2: Waiting for lock... timeout after 5 minutes<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Solution:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># In CodePipeline:\nconcurrent_build_limit: 1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or better &#8211; queue builds:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Build 1 running \u2192 Build 2 queued\nBuild 1 completes \u2192 Build 2 starts<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What I Learned<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Separate state files are essential &#8211; Never share state between environments<\/li>\n\n\n\n<li>DynamoDB locking prevents disasters &#8211; Concurrent applies can corrupt state<\/li>\n\n\n\n<li>Backend configs enable multi-env &#8211; Same code, different backends<\/li>\n\n\n\n<li>Parameter Store for secrets &#8211; Never hardcode credentials<\/li>\n\n\n\n<li>Buildspec phases matter &#8211; Install, pre-build, build, post-build structure<\/li>\n\n\n\n<li>Auto-approve is dangerous &#8211; Especially for destroy commands<\/li>\n\n\n\n<li>Version consistency critical &#8211; Terraform version must match state file<\/li>\n\n\n\n<li>CodeBuild IAM needs careful setup &#8211; Must have all necessary permissions<\/li>\n\n\n\n<li>Pipeline feedback is fast &#8211; See failures in minutes, not hours<\/li>\n\n\n\n<li>Infrastructure as Code is powerful &#8211; Click a button, deploy entire environment<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>After building and manually deploying infrastructure, I faced the real DevOps question: &#8220;How do I deploy infrastructure changes safely across multiple environments&hellip;<\/p>\n","protected":false},"author":2,"featured_media":154,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"6","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/infrastructure-code-devops-automated-deployment-aws-reza-chegini-og5pc\/","footnotes":""},"categories":[85,10],"tags":[60,30,95,8,55,87],"series":[86],"class_list":["post-152","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aws","category-devops","tag-aws","tag-ci-cd","tag-codepipeline","tag-devops","tag-iac","tag-terraform","series-aws-with-terraform"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/infrastructure-code-devops-codepipeline-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/152","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=152"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/152\/revisions"}],"predecessor-version":[{"id":153,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/152\/revisions\/153"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/154"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=152"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=152"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=152"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=152"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}