{"id":381,"date":"2026-10-09T18:19:47","date_gmt":"2026-10-09T22:19:47","guid":{"rendered":"https:\/\/securebydefault.io\/blog\/?p=381"},"modified":"2026-10-09T19:15:57","modified_gmt":"2026-10-09T23:15:57","slug":"import-existing-aws-infrastructure-terraform","status":"publish","type":"post","link":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/","title":{"rendered":"Importing My Cloud Resume into Terraform Without Taking the Site Down"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg\" alt=\"\" class=\"wp-image-386\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-300x169.jpg 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-768x432.jpg 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><em>What broke, what I found, and the three things I deleted<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">My portfolio site has been running on AWS for a while now: S3, CloudFront, Route 53, plus a small serverless backend (Lambda, API Gateway, DynamoDB) that counts visitors. The backend was built with SAM. The rest was a mix of console clicking and good intentions. It works, but if you asked me to rebuild it from scratch tomorrow, I would be doing a lot of remembering and a lot of squinting at the console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So I put the whole thing into Terraform. Not a fresh copy sitting next to the old one, but the actual live infrastructure, imported into state, with no downtime. This post covers how that went, the errors I hit (several were my own fault), and the part I did not expect: I found things in my own AWS account that I never put there on purpose.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-1 wp-block-paragraph\"><strong>Who this is for<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">This is <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\"><strong>not<\/strong> <\/mark>a from-scratch Cloud Resume Challenge tutorial. It is about bringing existing, live AWS infrastructure under Terraform management without rebuilding it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You will get the most out of it if you have:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Basic AWS familiarity (you know what S3, Lambda and IAM are)<\/li>\n\n\n\n<li>The AWS CLI installed and configured<\/li>\n\n\n\n<li>Terraform installed<\/li>\n\n\n\n<li>Some existing AWS infrastructure you would like to manage with Terraform<\/li>\n\n\n\n<li>Git<\/li>\n\n\n\n<li>Enough comfort reading a terraform plan to spot the word &#8220;destroy&#8221;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Project repositories<\/p>\n\n\n\n<div style=\"overflow-x:auto; margin: 24px 0;\">\n  <table style=\"\n    width:100%;\n    border-collapse:collapse;\n    background:#1f1f1f;\n    color:#f3f4f6;\n    font-family:Arial, Helvetica, sans-serif;\n    font-size:16px;\n    line-height:1.45;\n    border:1px solid #5b6573;\n  \">\n    <thead>\n      <tr style=\"background:#cbd5e1; color:#1f2937;\">\n        <th style=\"\n          text-align:left;\n          padding:12px 14px;\n          border:1px solid #5b6573;\n          width:34%;\n          font-weight:700;\n        \">\n          Repo\n        <\/th>\n        <th style=\"\n          text-align:left;\n          padding:12px 14px;\n          border:1px solid #5b6573;\n          font-weight:700;\n        \">\n          What it holds\n        <\/th>\n      <\/tr>\n    <\/thead>\n\n    <tbody>\n      <tr>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          <a href=\"https:\/\/github.com\/RonMercier\/terraform-cloud-resume\"\n             target=\"_blank\"\n             rel=\"noopener noreferrer\"\n             style=\"color:#67b7ff; text-decoration:underline;\">\n            terraform-cloud-resume\n          <\/a>\n        <\/td>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          The main repo for this post: Terraform for the infrastructure configuration\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          <a href=\"https:\/\/github.com\/RonMercier\/cloud-resume-backend\"\n             target=\"_blank\"\n             rel=\"noopener noreferrer\"\n             style=\"color:#67b7ff; text-decoration:underline;\">\n            cloud-resume-backend\n          <\/a>\n        <\/td>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          Lambda code, SAM template and the backend GitHub Actions pipeline\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          <a href=\"https:\/\/github.com\/RonMercier\/AWS-S3-Static-Website\"\n             target=\"_blank\"\n             rel=\"noopener noreferrer\"\n             style=\"color:#67b7ff; text-decoration:underline;\">\n            AWS-S3-Static-Website\n          <\/a>\n        <\/td>\n        <td style=\"padding:12px 14px; border:1px solid #5b6573; vertical-align:top;\">\n          The website files and the frontend GitHub Actions pipeline\n        <\/td>\n      <\/tr>\n    <\/tbody>\n  <\/table>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-2 wp-block-paragraph\"><strong>What is actually deployed<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"606\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-1024x606.png\" alt=\"architecture overview\" class=\"wp-image-380\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-1024x606.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-300x178.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-768x455.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-1536x910.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/architecture-overview-2048x1213.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Visitors reach the site through Route 53 and CloudFront, which pulls from S3. The browser then calls API Gateway, which invokes Lambda, which reads and writes DynamoDB.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One thing I want to be clear about, because it is easy to get wrong: Terraform does not own everything here.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"671\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-1024x671.png\" alt=\"\" class=\"wp-image-379\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-1024x671.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-300x196.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-768x503.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-1536x1006.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/ownership-map-2048x1341.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\"><strong>Terraform<\/strong> <\/mark>owns the infrastructure configuration: the DynamoDB table, IAM role, Lambda function settings, API Gateway, S3 bucket settings, CloudFront and the Route 53 records.<\/li>\n\n\n\n<li><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">SAM and GitHub Actions<\/mark><\/strong> (backend repo) own the Lambda application code.<\/li>\n\n\n\n<li><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">GitHub Actions<\/mark><\/strong> (frontend repo) owns the website files in the bucket.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The SAM-created CloudFormation stack (portfolio-app) still exists and still tracks some of these resources. I come back to that at the end, because it is an open item and not a solved one.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-3 wp-block-paragraph\"><strong>The rule: import first, improve second<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Importing live infrastructure is a bit like changing a tire on a moving car, except the car is your resume. So I stuck to one workflow for every single resource:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Look at the real thing with the AWS CLI (describe-table, get-function, and friends).<\/li>\n\n\n\n<li>Write Terraform that matches it.<\/li>\n\n\n\n<li>Run terraform import.<\/li>\n\n\n\n<li>Run terraform plan, and touch nothing until it says &#8220;No changes&#8221; or shows a difference I fully understand.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Only after a resource is clean do I change anything on purpose. That way every improvement shows up as its own small, readable diff instead of being buried inside the import.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-4 wp-block-paragraph\"><strong>What terraform import actually does<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you have not used it before, here is the part that trips people up: terraform import does <strong>not<\/strong> write any configuration for you. It does not change the AWS resource either. All it does is record in Terraform&#8217;s state that &#8220;this resource address in my code is that real thing in AWS.&#8221; You still write the configuration yourself.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"138\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-1024x138.png\" alt=\"\" class=\"wp-image-378\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-1024x138.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-300x40.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-768x103.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-1536x206.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/import-flow-2048x275.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That is why the plan afterward matters so much. The plan compares the configuration you wrote against the real resource. If they match, you get &#8220;No changes&#8221; and a clean baseline. If they do not, Terraform tells you exactly where.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">During the import phase, if the first plan shows an unexpected difference, make the Terraform configuration match the existing live resource until you have a clean baseline. Make intentional infrastructure changes only after that.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-5 wp-block-paragraph\"><strong>Walkthrough 1: the DynamoDB table (the warm-up)<\/strong><\/p>\n\n\n\n<p class=\"has-custom-css wp-custom-css-9361e366 wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Find the real thing:<\/mark><\/strong><\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>aws dynamodb describe-table --table-name visitorCount<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">That gave me the key schema (id, string), the billing mode (pay per request) and deletion protection (on).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Write a matching resource<\/strong> (<mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">dynamodb.tf<\/mark>), with the resource address<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws_dynamodb_table.visitor_count<\/mark>:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>resource \"aws_dynamodb_table\" \"visitor_count\" {\n  name         = \"visitorCount\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"id\"\n\n  attribute {\n    name = \"id\"\n    type = \"S\"\n  }\n\n  deletion_protection_enabled = true\n}<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Import it.<\/strong> For a DynamoDB table the import ID is just the table name:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">terraform import aws_dynamodb_table.visitor_count visitorCount<\/mark><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Plan.<\/strong> My first plan after import showed one change: Terraform wanted to add two tags (<mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Project <\/mark>and <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Environment<\/mark>). I had put them in my config, and the live table did not have them. Honest note: that bends my own rule, since I changed something on purpose before the baseline was clean. It was two tags on a table, I understood the diff completely, and I kept them. The tags are in the repo now. If you want to follow the rule strictly, delete the tags from your config until the plan is clean, then add them back as their own change.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-6 wp-block-paragraph\"><strong>Walkthrough 2: an API Gateway route<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color wp-elements-7 wp-block-paragraph\">Find the real thing:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>aws apigatewayv2 get-apis\naws apigatewayv2 get-routes --api-id &lt;api-id&gt;<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The route list gives you a route ID alongside the route key. (The output is trimmed here, and I am showing the command shape rather than pasting my account&#8217;s output.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Write a matching resource<\/strong>, address <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws_apigatewayv2_route.count_get<\/mark>:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>resource \"aws_apigatewayv2_route\" \"count_get\" {\n  api_id             = aws_apigatewayv2_api.http_api.id\n  route_key          = \"GET \/count\"\n  target             = \"integrations\/${aws_apigatewayv2_integration.lambda.id}\"\n  authorization_type = \"NONE\"\n}<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Import it.<\/strong> API Gateway v2 routes use <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">api-id\/route-id<\/mark> as the import ID:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>terraform import aws_apigatewayv2_route.count_get &lt;api-id&gt;\/&lt;route-id&gt;<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Plan.<\/strong> After importing the API, integration, routes and stage, the plan came back clean. That is where it got interesting.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-8 wp-block-paragraph\">Setup: three small speed bumps<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">Installing Terraform<\/mark><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">My first attempt to add the HashiCorp repo failed with a <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">NO_PUBKEY<\/mark> error. I had piped the key download straight into <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">gpg<\/mark>, and the key never saved correctly. The fix was boring: download the key to a file first, then dearmor it as a separate step. Piping is great until it fails silently.<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>wget -O \/tmp\/hashicorp.gpg https:\/\/apt.releases.hashicorp.com\/gpg\nsudo gpg --dearmor -o \/usr\/share\/keyrings\/hashicorp-archive-keyring.gpg \n\/tmp\/hashicorp.gpg<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">Credentials that expired overnight<\/mark><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The AWS CLI worked fine on day one. The next day: <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">InvalidClientTokenId<\/mark>. Same machine, same config, nothing changed. The clue was in the access key ID prefix. Keys starting with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">AKIA <\/mark>are long-term IAM user keys. Keys starting with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">ASIA <\/mark>are temporary session credentials that expire. Mine started with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">ASIA<\/mark>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was a dedicated IAM user (<mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">terraform-deploy<\/mark>) with a long-term key, used for this migration. Two details worth knowing. First, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws configure<\/mark> does not remove an old <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws_session_token<\/mark> line from the credentials file, so check for one or the stale token gets paired with your new key. Second, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws sts get-caller-identity<\/mark> is the fastest way to confirm who AWS thinks you are before running anything bigger. While I was in there I also fixed the default region, which had quietly been set to us-east-1 when everything lives in us-east-2.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A note on that fix, because I do not want it read as a recommendation: a long-term key on a server is what I used here, not what I would pick for a fresh setup. If you are starting clean, short-lived credentials are the better habit: IAM Identity Center (SSO) or an assumed role, so nothing long-lived sits on disk. If you do use a long-term key for a migration like mine, treat it as temporary and remove it when the work is done.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">Remote state first<\/mark><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before touching the real infrastructure, I bootstrapped the state backend with a tiny separate config: an S3 bucket (versioned, encrypted, public access blocked, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">prevent_destroy<\/mark> on) and a DynamoDB table for state locking. Running the bootstrap with local state avoids the chicken-and-egg problem of Terraform needing a backend to create its own backend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On locking, here is where things stand. I used DynamoDB locking during this migration, and the repo still does. Terraform warns that <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">dynamodb_table<\/mark> is deprecated, and current versions of the S3 backend support native locking with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">use_lockfile = true<\/mark>. If you are setting this up fresh, evaluate <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">use_lockfile<\/mark> first. I have not moved my own setup yet, and I did not remove the DynamoDB lock table, so treat that as a future change and not something I have done.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-9 wp-block-paragraph\"><strong>The backend half: DynamoDB, IAM, Lambda, API Gateway<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">DynamoDB was the warm-up, covered above.<\/mark><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The IAM role and policy produced a scarier diff, with Terraform appearing to remove and re-add the same permissions. It was only rendering a change to a JSON string. The one real difference was the explicit policy Version field. Cosmetic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">Lambda was the first real error:<\/mark><\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>Error: one of `filename,image_uri,s3_bucket` must be specified<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The Lambda resource requires a code source even when you plan to ignore code changes. My SAM and GitHub Actions pipeline already owns deployments, so I pointed <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">filename <\/mark>at a throwaway <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">placeholder.zip<\/mark> and told Terraform to ignore <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">filename <\/mark>and <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">source_code_hash<\/mark>. Terraform manages the function&#8217;s configuration, the pipeline manages its code, and nobody fights.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">API Gateway took six imports: the API, the integration, three routes, and the stage.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"866\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-1024x866.png\" alt=\"\" class=\"wp-image-375\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-1024x866.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-300x254.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-768x649.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-1536x1299.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/backend-current-2048x1732.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-10 wp-block-paragraph\"><strong>The finding: routes and a permission nobody asked for<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">When I listed the API&#8217;s routes, there were three:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">GET \/count<\/mark> (the one my site actually uses)<\/li>\n\n\n\n<li><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">ANY \/<\/mark><\/li>\n\n\n\n<li><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">ANY \/{proxy+}<\/mark><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Then I pulled the Lambda&#8217;s resource policy and found two invoke permissions. One was the SAM-generated permission, scoped to <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">GET \/count<\/mark> only. The other allowed API Gateway to invoke the function from any stage, any method, any path, and its name did not look like anything SAM generates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Together, those extras meant every path and method on the API reached my Lambda, not just the one request my site makes. For a visitor counter this is not a breach and not a disaster. It is exactly the kind of drift that makes an attack surface bigger than anyone intended.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before deleting anything I checked two things:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">The SAM template.<\/mark><\/strong> It defines exactly one event, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">GET \/count<\/mark>, so the pipeline would not recreate the extras.<\/li>\n\n\n\n<li><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">The frontend.<\/mark><\/strong> <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">index.html<\/mark> calls the <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">\/count<\/mark> endpoint directly. Removing the catch-alls could not break the site.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">I removed the two routes and the broad permission through Terraform. The plan said 0 to add, 0 to change, 3 to destroy, and nothing else. Then I tested from the outside:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n\/* SecureByDefault code blocks *\/\n.sbd-code-block,\n.sbd-code-block pre,\n.sbd-code-block code {\n    background: #2b2f32 !important;\n    background-color: #2b2f32 !important;\n    color: #f5f7fa !important;\n}\n\n.sbd-code-block {\n    margin: 24px 0;\n    border: 1px solid #3b4248;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.sbd-code-block pre {\n    margin: 0 !important;\n    padding: 18px 20px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 15px !important;\n    line-height: 1.55 !important;\n    white-space: pre !important;\n    overflow-x: auto !important;\n    border: 0 !important;\n    box-shadow: none !important;\n}\n\n.sbd-code-block code {\n    padding: 0 !important;\n    border: 0 !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: inherit !important;\n    white-space: inherit !important;\n}\n\n\/* SecureByDefault inline code *\/\n.sbd-inline-code {\n    display: inline !important;\n    background: #303840 !important;\n    color: #8fdcff !important;\n    border: 1px solid #46515b !important;\n    border-radius: 4px !important;\n    padding: 2px 5px !important;\n    font-family: Consolas, \"Courier New\", monospace !important;\n    font-size: 0.92em !important;\n    white-space: normal !important;\n}\n<\/style>\n\n<div class=\"sbd-code-block\">\n<pre><code>GET  \/count        returns 200\nGET  \/prod\/count   returns 404\nPOST \/count        returns 404<\/code><\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The real endpoint works and everything else is gone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That<mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\"> \/prod\/count<\/mark> result deserves its own mention. My earlier API security assessment tested a <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">\/prod\/count<\/mark> path. This API&#8217;s stage is <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">$default<\/mark>, so I believe that path was being served by the catch-all route, not by the route I thought I was testing. (That is an inference from the before and after behavior, not something I traced request by request.) The assessment&#8217;s test scripts now need an update. Codifying infrastructure does not just document it, it questions it.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-11 wp-block-paragraph\"><strong>The frontend half: S3, CloudFront, Route 53<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The front end had a smaller footprint than I expected: one bucket, one CloudFront distribution, and a hosted zone with a handful of records. I made two deliberate choices. The Route 53 hosted zone is a read-only data source, because a mistake there can take a domain offline. And the DNS validation records for the live certificate are managed with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">prevent_destroy<\/mark>, so nobody can accidentally break certificate renewal.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"798\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-1024x798.png\" alt=\"\" class=\"wp-image-376\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-1024x798.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-300x234.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-768x599.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-1536x1197.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-current-2048x1597.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Worth knowing about how this is wired today: CloudFront talks to the bucket&#8217;s S3 <strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">website endpoint<\/mark><\/strong> over HTTP, and the bucket policy allows reads based on a <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Referer <\/mark>header that CloudFront adds. It works, and it is how the site was built originally. It is also a legacy pattern, and a Referer header is not strong access control. I imported it as it is. Improving it is Part 2.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">The CLI pager gotcha.<\/mark><\/strong> A long <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">get-distribution-config<\/mark> output stopped at a lone colon, because the CLI had opened it in <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">less<\/mark>. Running <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws configure set cli_pager &#8220;&#8221;<\/mark> fixed it for every future command too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\"><strong>ACM clutter.<\/strong> <\/mark>There were three certificates for the same domain, two expired and unused. Only one is attached to CloudFront. I left the expired ones alone for now rather than guess.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">The final plan<\/mark>.<\/strong> After 12 imports, the only difference was a tag. The distribution has a <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Name <\/mark>tag I had left out of my config, and its value starts with a leading space. I matched it exactly so the first plan stayed clean. Fixing the space is a separate change for later.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-12 wp-block-paragraph\"><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-2-color\">Mistakes along the way<\/mark><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform is great at telling you what went wrong. It is less great at telling you it was you. Here is the full list:<\/p>\n\n\n\n<div style=\"overflow-x:auto; margin:24px 0;\">\n  <table style=\"\n    width:100%;\n    border-collapse:collapse;\n    background:#242424;\n    color:#f3f4f6;\n    font-family:Arial, Helvetica, sans-serif;\n    font-size:15px;\n    line-height:1.45;\n    border:1px solid #5b6573;\n  \">\n    <thead>\n      <tr style=\"background:#cbd5e1; color:#111827;\">\n        <th style=\"\n          text-align:left;\n          padding:11px 12px;\n          border:1px solid #5b6573;\n          width:29%;\n          font-weight:700;\n        \">\n          What I saw\n        <\/th>\n\n        <th style=\"\n          text-align:left;\n          padding:11px 12px;\n          border:1px solid #5b6573;\n          width:36%;\n          font-weight:700;\n        \">\n          What it actually was\n        <\/th>\n\n        <th style=\"\n          text-align:left;\n          padding:11px 12px;\n          border:1px solid #5b6573;\n          width:35%;\n          font-weight:700;\n        \">\n          The fix\n        <\/th>\n      <\/tr>\n    <\/thead>\n\n    <tbody>\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">NO_PUBKEY<\/span>\n          on\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">apt update<\/span>\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Key piped into\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">gpg<\/span>,\n          never saved\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Download to a file, then\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">gpg &#8211;dearmor<\/span>\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">InvalidClientTokenId<\/span>\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Temporary ASIA credentials expired\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Long-term key on a dedicated IAM user, clear any stale session token\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Lambda needs a code source\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Schema requires one even with\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">ignore_changes<\/span>\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Placeholder zip plus\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">ignore_changes<\/span>\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Variable\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">domain_name<\/span>\n          not declared\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          I created the new\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">.tf<\/span>\n          files in my home directory, not the project folder\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Move the files into the project folder\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Backend\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">init<\/span>:\n          invalid bucket name\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          I pasted literal placeholder text into\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">backend.hcl<\/span>\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Use the real bucket name, then\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">init -reconfigure<\/span>\n        <\/td>\n      <\/tr>\n\n      <tr>\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          Git push rejected (fetch first)\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          GitHub had auto-created a README\n        <\/td>\n\n        <td style=\"padding:11px 12px; border:1px solid #5b6573; vertical-align:top;\">\n          <span style=\"font-family:Consolas,'Courier New',monospace; color:#7dd3fc;\">git pull &#8211;rebase<\/span>,\n          then push\n        <\/td>\n      <\/tr>\n    <\/tbody>\n  <\/table>\n<\/div>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color wp-elements-13 wp-block-paragraph\"><strong>Publishing it without leaking things<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The repo is public, so before the first push I set up a few guardrails:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">.gitignore<\/mark> for state files, the <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">.terraform\/<\/mark> folder, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">*.tfvars<\/mark>, and a local <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">backend.hcl<\/mark>.<\/li>\n\n\n\n<li>Backend settings live in <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">backend.hcl<\/mark> and get passed with <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">terraform init -backend-config<\/mark>, so the committed backend block is empty.<\/li>\n\n\n\n<li>The ACM certificate ARN goes in <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">terraform.tfvars<\/mark>, also ignored.<\/li>\n\n\n\n<li>A quick <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">grep <\/mark>for account IDs and key prefixes before every commit.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">State files deserve the most respect of the lot. They can contain sensitive values in plain text, which is why they live in a private, encrypted, versioned bucket and never in git.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That grep earned its keep. My bootstrap config still had my AWS account ID hardcoded in the bucket name. An account ID is not a secret the way a password or access key is: knowing it does not let anyone in. But it is still information I would rather not publish, and I redact it from screenshots, so the repo should follow the same rule. The fix was an <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">aws_caller_identity<\/mark> data source, which produces the same bucket name without hardcoding anything, and the plan confirmed no change. The ID still exists in the first commit&#8217;s history. Since it is not a credential, I fixed forward instead of rewriting history.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For pushing from a server, I used an SSH deploy key scoped to this one repository, with an SSH config alias so the key is only used for that repo. That beats leaving a personal access token on a production box.<\/p>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-14 wp-block-paragraph\"><strong>What I would tell anyone doing this<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Import first, improve second. Keep the imports boring.<\/li>\n\n\n\n<li>During the import phase, if a plan shows a difference, make your config match the live resource until the baseline is clean. Make intentional changes after that.<\/li>\n\n\n\n<li>Read the plan for the word &#8220;destroy&#8221; before every apply. Every time.<\/li>\n\n\n\n<li>Let the import question your assumptions. Mine surfaced three things I did not know were there.<\/li>\n\n\n\n<li>Use <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">ignore_changes<\/mark> to keep Terraform and your deploy pipeline out of each other&#8217;s way.<\/li>\n\n\n\n<li>Treat the repo as public from the first commit.<\/li>\n<\/ul>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-15 wp-block-paragraph\"><strong>Where this stands, honestly<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">This repo is a migration case study, not a reference architecture. Some of what it describes is legacy on purpose, because the goal was a clean baseline first:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The S3 website endpoint, <mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-ast-global-color-0-color\">Referer <\/mark>header pattern and HTTP\/1.1 CloudFront setting are imported as they were.<\/li>\n\n\n\n<li>The SAM template in the backend repo has not been brought in line with the live API (stage name, CORS settings and a few other properties differ), and the CloudFormation stack still tracks the same resources Terraform now manages. That overlap is the next thing I want to clean up, so it is not a good idea to edit those resources from both sides in the meantime.<\/li>\n\n\n\n<li>State locking still uses DynamoDB.<\/li>\n<\/ul>\n\n\n\n<p class=\"has-ast-global-color-2-color has-text-color has-link-color has-medium-font-size wp-elements-16 wp-block-paragraph\"><strong>What&#8217;s next<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Part 2 is hardening, and none of it is done yet.  This involves moving CloudFront to Origin Access Control with a private S3 origin, cleaning up the expired certificates, sorting out the SAM and Terraform overlap, and adding Terraform to my GitHub Actions pipeline so plans run on pull requests.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"674\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-1024x674.png\" alt=\"\" class=\"wp-image-377\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-1024x674.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-300x198.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-768x506.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-1536x1012.png 1536w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/frontend-planned-oac-2048x1349.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That diagram is the plan, not the current state. After that, it is on to Docker and Kubernetes for the rest of this series.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The code is on GitHub: <a href=\"https:\/\/github.com\/RonMercier\/terraform-cloud-resume\">github.com\/RonMercier\/terraform-cloud-resume<\/a><\/p>\n\n\n\n<div style=\"\n  margin:32px 0 24px;\n  padding:28px 26px;\n  background:#081321;\n  border:1px solid #17466a;\n  border-radius:12px;\n  color:#eef5ff;\n  font-family:Arial, Helvetica, sans-serif;\n  box-shadow:0 0 24px rgba(0,212,255,0.08);\n\">\n  <h3 style=\"\n    margin:0 0 12px;\n    font-size:26px;\n    line-height:1.25;\n    color:#ffffff;\n  \">\n    Want more hands-on cloud projects like this?\n  <\/h3>\n\n  <p style=\"\n    margin:0 0 20px;\n    font-size:16px;\n    line-height:1.65;\n    color:#9fc5df;\n  \">\n    I share practical Linux, AWS, troubleshooting, cloud, and cybersecurity lessons every Tuesday, along with remote tech opportunities and career-focused resources.\n  <\/p>\n\n  <a href=\"https:\/\/newsletter.securebydefault.io\/\"\n     target=\"_blank\"\n     rel=\"noopener noreferrer\"\n     style=\"\n       display:inline-block;\n       padding:13px 20px;\n       background:#00d4ff;\n       color:#03111d;\n       text-decoration:none;\n       font-weight:700;\n       font-size:16px;\n       border-radius:8px;\n     \">\n    Join the newsletter + get the free Linux Starter Kit\n  <\/a>\n<\/div>\n\n\n\n<div style=\"\n  margin:24px 0 32px;\n  padding:22px 24px;\n  background:#0a1628;\n  border-left:4px solid #00d4ff;\n  border-radius:8px;\n  color:#eef5ff;\n  font-family:Arial, Helvetica, sans-serif;\n\">\n  <h4 style=\"\n    margin:0 0 8px;\n    font-size:21px;\n    color:#ffffff;\n  \">\n    Want to inspect the code?\n  <\/h4>\n\n  <p style=\"\n    margin:0 0 14px;\n    font-size:15px;\n    line-height:1.6;\n    color:#9fc5df;\n  \">\n    View the Terraform configuration, imports, state setup, and AWS infrastructure used in this project.\n  <\/p>\n\n  <a href=\"https:\/\/github.com\/RonMercier\/terraform-cloud-resume\"\n     target=\"_blank\"\n     rel=\"noopener noreferrer\"\n     style=\"\n       color:#55dfff;\n       font-weight:700;\n       text-decoration:none;\n       font-size:16px;\n     \">\n    View the Terraform project on GitHub \u2192\n  <\/a>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>I moved my live AWS Cloud Resume infrastructure into Terraform without rebuilding the site or taking it offline. Along the way, I hit credential issues, import problems, and uncovered extra API Gateway routes and a broader Lambda permission I did not know were there.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[86,153,152,1],"tags":[58,52,65,54,56,60,169,55,170,168],"class_list":["post-381","post","type-post","status-publish","format-standard","hentry","category-career-development","category-cloud-career","category-it-career","category-uncategorized","tag-api-gateway","tag-aws","tag-cloud-resume-challenge","tag-cloudfront","tag-dynamodb","tag-github-actions","tag-infrastructure-as-code","tag-lambda","tag-route-53","tag-terraform"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How to Import Existing AWS Infrastructure into Terraform<\/title>\n<meta name=\"description\" content=\"Learn how I imported a live AWS Cloud Resume into Terraform without downtime, cleaned up API Gateway drift, and built a safer Infrastructure as Code baseline.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"I Imported My Live AWS Cloud Resume into Terraform\" \/>\n<meta property=\"og:description\" content=\"A real-world Terraform migration with no downtime, real mistakes, API Gateway drift, and lessons from importing existing AWS infrastructure.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/\" \/>\n<meta property=\"og:site_name\" content=\"SecureByDefault\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-09T22:19:47+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-09T23:15:57+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"675\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Ron Mercier\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ron Mercier\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"14 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/\"},\"author\":{\"name\":\"Ron Mercier\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"headline\":\"Importing My Cloud Resume into Terraform Without Taking the Site Down\",\"datePublished\":\"2026-10-09T22:19:47+00:00\",\"dateModified\":\"2026-10-09T23:15:57+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/\"},\"wordCount\":2786,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"image\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg\",\"keywords\":[\"API Gateway\",\"AWS\",\"Cloud Resume Challenge\",\"CloudFront\",\"DynamoDB\",\"GitHub Actions\",\"Infrastructure as Code\",\"Lambda\",\"Route 53\",\"Terraform\"],\"articleSection\":[\"Career Development\",\"Cloud Career\",\"IT Career\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/\",\"name\":\"How to Import Existing AWS Infrastructure into Terraform\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg\",\"datePublished\":\"2026-10-09T22:19:47+00:00\",\"dateModified\":\"2026-10-09T23:15:57+00:00\",\"description\":\"Learn how I imported a live AWS Cloud Resume into Terraform without downtime, cleaned up API Gateway drift, and built a safer Infrastructure as Code baseline.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#primaryimage\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg\",\"contentUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg\",\"width\":1200,\"height\":675},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/import-existing-aws-infrastructure-terraform\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Importing My Cloud Resume into Terraform Without Taking the Site Down\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/\",\"name\":\"SecureByDefault\",\"description\":\"Cloud Security &amp; Cybersecurity for IT Professionals\",\"publisher\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\",\"name\":\"Ron Mercier\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"contentUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"width\":512,\"height\":512,\"caption\":\"Ron Mercier\"},\"logo\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\"},\"sameAs\":[\"https:\\\/\\\/securebydefault.io\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/ron-mercier\\\/\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCDyWOTMI23S8Y3zwPoX3UkQ\"],\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/author\\\/sbd_admin\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How to Import Existing AWS Infrastructure into Terraform","description":"Learn how I imported a live AWS Cloud Resume into Terraform without downtime, cleaned up API Gateway drift, and built a safer Infrastructure as Code baseline.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/","og_locale":"en_US","og_type":"article","og_title":"I Imported My Live AWS Cloud Resume into Terraform","og_description":"A real-world Terraform migration with no downtime, real mistakes, API Gateway drift, and lessons from importing existing AWS infrastructure.","og_url":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/","og_site_name":"SecureByDefault","article_published_time":"2026-10-09T22:19:47+00:00","article_modified_time":"2026-10-09T23:15:57+00:00","og_image":[{"width":1200,"height":675,"url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg","type":"image\/jpeg"}],"author":"Ron Mercier","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Ron Mercier","Est. reading time":"14 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#article","isPartOf":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/"},"author":{"name":"Ron Mercier","@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"headline":"Importing My Cloud Resume into Terraform Without Taking the Site Down","datePublished":"2026-10-09T22:19:47+00:00","dateModified":"2026-10-09T23:15:57+00:00","mainEntityOfPage":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/"},"wordCount":2786,"commentCount":0,"publisher":{"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"image":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#primaryimage"},"thumbnailUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg","keywords":["API Gateway","AWS","Cloud Resume Challenge","CloudFront","DynamoDB","GitHub Actions","Infrastructure as Code","Lambda","Route 53","Terraform"],"articleSection":["Career Development","Cloud Career","IT Career"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/","url":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/","name":"How to Import Existing AWS Infrastructure into Terraform","isPartOf":{"@id":"https:\/\/securebydefault.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#primaryimage"},"image":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#primaryimage"},"thumbnailUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1-1024x576.jpg","datePublished":"2026-10-09T22:19:47+00:00","dateModified":"2026-10-09T23:15:57+00:00","description":"Learn how I imported a live AWS Cloud Resume into Terraform without downtime, cleaned up API Gateway drift, and built a safer Infrastructure as Code baseline.","breadcrumb":{"@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#primaryimage","url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg","contentUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/10\/importing-aws-into-terraform-without-downtime-1200x675-1.jpg","width":1200,"height":675},{"@type":"BreadcrumbList","@id":"https:\/\/securebydefault.io\/blog\/import-existing-aws-infrastructure-terraform\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/securebydefault.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Importing My Cloud Resume into Terraform Without Taking the Site Down"}]},{"@type":"WebSite","@id":"https:\/\/securebydefault.io\/blog\/#website","url":"https:\/\/securebydefault.io\/blog\/","name":"SecureByDefault","description":"Cloud Security &amp; Cybersecurity for IT Professionals","publisher":{"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/securebydefault.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8","name":"Ron Mercier","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","contentUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","width":512,"height":512,"caption":"Ron Mercier"},"logo":{"@id":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png"},"sameAs":["https:\/\/securebydefault.io\/blog","https:\/\/www.linkedin.com\/in\/ron-mercier\/","https:\/\/www.youtube.com\/channel\/UCDyWOTMI23S8Y3zwPoX3UkQ"],"url":"https:\/\/securebydefault.io\/blog\/author\/sbd_admin\/"}]}},"_links":{"self":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/381","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/comments?post=381"}],"version-history":[{"count":19,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/381\/revisions"}],"predecessor-version":[{"id":405,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/381\/revisions\/405"}],"wp:attachment":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/media?parent=381"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/categories?post=381"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/tags?post=381"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}