IAC 03 이미 있는 것을 코드로 가져오기 — import
고친 사람 github-actions[bot]
실측은 전부 라즈베리파이 5 · Terraform v1.16.4 · hashicorp/aws 6.66.0 에서 돌렸습니다. 진짜 AWS 대신 AWS API 를 흉내 내는 moto 서버(5.2.3.dev0) 를
127.0.0.1:4566에 띄워 겨냥했습니다. 흉내라서 진짜 AWS 와 다를 수 있는 곳은 결과 옆에 적었습니다. 출력은 실제로 찍힌 것을 붙였고, 길어서 자른 곳은 「…」로 표시했습니다.
콘솔에서 손으로 만들어 몇 달째 돌고 있는 보안 그룹(AWS 에서 서버 앞에 붙이는 방화벽 규칙 묶음)이 하나 있습니다. 이걸 테라폼으로 관리하고 싶어서 짧은 코드를 쓰고, 「이미 있는 이 자원을 가져와라」는 지시(import — 손으로 만든 자원을 테라폼 관리 아래로 들이는 일)를 붙인 뒤 plan(무엇을 바꿀지 미리 보여 주는 명령)을 돌렸습니다. 돌아온 계획의 한가운데에 이런 줄이 있었습니다.
# aws_security_group.app must be replaced
# (imported from "sg-48742ecc501d16f90")
# Warning: this will destroy the imported resource
-/+ resource "aws_security_group" "app" {
~ description = "launch-wizard-1 created 2026-07-01" -> "demo app sg" # forces replacement
가져오려던 보안 그룹을 지우고 새로 만들겠다는 계획입니다. 원인은 설명(description) 문자열 하나였습니다. 콘솔 마법사가 붙여 둔 launch-wizard-1 created 2026-07-01 과 코드에 적은 demo app sg 가 달랐습니다.
이 편은 그 경고가 왜 나왔는지, 이 계획이 운영 중인 서버에 무엇을 하려 했는지, 그리고 계획이 「바꿀 것 없음(No changes)」이 될 때까지 무엇을 몇 번 고쳤는지를 따라갑니다. 길은 두 가지를 걸어 봅니다. 하나는 사람이 직접 쓴 코드로 가져오는 길이고, 다른 하나는 테라폼이 코드 초안을 뽑아 주는 길입니다.
1. 테라폼은 무엇을 보고 「만들지 말지」를 정하나
이 편에 나오는 낱말
테라폼(Terraform) 은 서버·방화벽·저장소 같은 클라우드 자원을 코드로 적어 두면, 실제 클라우드를 그 코드대로 맞춰 주는 도구입니다. 이렇게 인프라를 코드로 다루는 방식을 코드형 인프라라 합니다. 이 편에서 쓰는 테라폼 낱말은 여섯 개입니다.
| 낱말 | 한 줄 |
|---|---|
| HCL | 테라폼 코드를 쓰는 언어. 종류 "이름" { 키 = 값 } 모양의 블록을 쌓습니다 |
| 프로바이더(provider) | 테라폼이 AWS 같은 클라우드 API 를 부를 때 쓰는 플러그인. 여기서는 hashicorp/aws 입니다 |
resource 블록 |
「자원 하나가 이런 모양이어야 한다」는 선언. aws_security_group.app 처럼 종류.이름 이 그 블록의 주소입니다 |
| 상태 파일 | 테라폼이 「내가 관리하는 코드 주소 ↔ 실제 자원 ID」를 적어 두는 JSON 파일(terraform.tfstate) |
plan |
코드·상태 파일·실제 자원을 비교해 무엇을 만들고(+) 바꾸고(~) 지울지(-) 계획만 보여 주는 명령 |
apply |
그 계획을 실제로 실행하는 명령 |
여기서 가장 중요한 것은 상태 파일입니다. 테라폼은 클라우드를 뒤져서 「비슷한 게 이미 있네」 하고 알아서 짝을 짓지 않습니다. 코드의 resource 블록 하나가 실제 자원 무엇과 짝인지는 상태 파일에 적힌 것만 믿습니다. 도서관 대출 장부를 떠올리시면 됩니다. 책이 서가에 꽂혀 있어도 장부에 없으면 사서는 그 책이 도서관 것인 줄 모릅니다.
가져올 것 — 손으로 만든 자원 셋
콘솔에서 손으로 만든 운영 서버의 흔한 모양을 aws CLI 로 흉내 냈습니다. 보안 그룹(EC2 앞에 붙는 방화벽 규칙 묶음) 하나, EC2 인스턴스(가상 서버) 하나, S3 버킷(파일 저장소) 하나입니다.
# handmade.sh (발췌)
SG_ID=$(aws ec2 create-security-group \
--group-name demo-sg \
--description "launch-wizard-1 created 2026-07-01" \
...)
# 22(SSH) 와 나중에 연 443(HTTPS)
for port in 22 443; do # 원본은 두 줄
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" --protocol tcp \
--port $port --cidr 0.0.0.0/0
done
IID=$(aws ec2 run-instances \
--image-id ami-0f47531f8c49bd1c6 \
--instance-type t4g.small \
--security-group-ids "$SG_ID" ...)
aws s3api create-bucket --bucket demo-logs
스크립트가 끝에 찍은 ID 셋입니다.
SG=sg-48742ecc501d16f90 INSTANCE=i-feaabc6f40e446585 BUCKET=demo-logs
--description— 보안 그룹의 설명입니다. 콘솔 마법사로 만들면launch-wizard-1 created …같은 문구가 자동으로 붙습니다--cidr 0.0.0.0/0— 「모든 IP 주소에서」라는 뜻입니다. SSH 가 전 세계에 열린 것은 손으로 만든 서버에서 자주 보이는 모양입니다- 보안 그룹과 인스턴스에는
Name태그(demo-sg·demo-app)도 붙였습니다. 위 발췌에서는 생략했습니다
이 셋은 테라폼이 전혀 모르는 자원입니다. 상태 파일에 한 줄도 없습니다.
2. 코드만 쓰고 apply 하면 — 충돌로 멈췄다
import 를 모르면 가장 먼저 해 보는 방법이 이것입니다. 실제와 똑같은 코드를 써 놓고 apply 하면 테라폼이 「이미 있네」 하고 넘어가 주지 않을까요.
빈 상태 파일로 시작하는 새 폴더에 실제와 똑같은 코드(4절 끝의 코드)를 두고, import 지시 없이 apply 했습니다. 이어서 aws CLI 로 자원 수를 셌습니다. --query 는 aws CLI 응답에서 필요한 값만 골라내는 옵션이고, 여기서는 length(…) 로 개수만 뽑았습니다. 마지막 terraform state list 는 상태 파일에 적힌 코드 주소를 나열하는 명령입니다.
실측 — import 없이 코드만 apply (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform apply -auto-approve -no-color
aws ec2 describe-security-groups \
--filters Name=group-name,Values=demo-sg \
--query 'length(SecurityGroups)'
aws ec2 describe-instances --filters \
Name=tag:Name,Values=demo-app \
Name=instance-state-name,Values=running \
--query 'length(Reservations[].Instances[])'
terraform state list -no-color
Plan: 3 to add, 0 to change, 0 to destroy.
aws_security_group.app: Creating...
aws_s3_bucket.logs: Creating...
Error: creating Security Group (demo-sg): operation error EC2: CreateSecurityGroup, https response error StatusCode: 400, RequestID: Tqn5xCINhAuSJThngraPKIArjBNz0CnDeizrg4tzwJL0q853WMl4, api error InvalidGroup.Duplicate: The security group 'demo-sg' already exists
…
Error: creating S3 Bucket (demo-logs): BucketAlreadyExists
…
### 자원 수
SG(demo-sg): 1 인스턴스(running, Name=demo-app): 1
(state list 끝)
Plan: 3 to add — 상태 파일이 비어 있으니 테라폼 눈에는 세 개 다 새로 만들 것입니다. 실제로 만들러 갔다가 이름이 겹쳐 실패했습니다. 보안 그룹은 InvalidGroup.Duplicate, 버킷은 BucketAlreadyExists 입니다. 인스턴스 코드는 보안 그룹 ID 를 참조하므로 보안 그룹이 먼저 만들어져야 합니다. 그래서 인스턴스는 보안 그룹을 기다리다 시작도 못 했습니다. 끝나고 세어 보니 보안 그룹 1개, 인스턴스 1대로 중복은 0이었습니다. (state list 끝) 앞에는 코드 주소가 하나도 찍히지 않았습니다. 상태 파일이 빈 채로 남았다는 뜻입니다.
이번에 멈춘 것은 자원 종류 덕입니다. 보안 그룹 이름은 AWS 가, 버킷 이름은 프로바이더가 먼저 확인해서 겹침을 막았습니다. 인스턴스처럼 이름으로 겹침을 막지 않는 자원만 있었다면 오류 없이 한 대가 더 생겼을 것입니다. 이 시리즈 02편 「plan 과 apply, 상태 파일」에서 상태 파일을 지우고 apply 를 되풀이했을 때 인스턴스가 1대 → 2대 → 3대로 늘어나는 것을 실측했습니다.
결론은 하나입니다. 코드를 실제와 똑같이 써도 상태 파일에 짝이 적혀 있지 않으면 테라폼은 만들러 갑니다. 짝을 적는 일이 import 입니다.
3. import 는 상태 파일에 짝을 적는 일이다
테라폼 1.5 부터 코드 안에 import 블록을 쓸 수 있습니다. 두 값만 받습니다.
import {
to = aws_security_group.app # 코드 주소
id = "sg-48742ecc501d16f90" # 실제 ID
}
import {
to = aws_instance.app
id = "i-feaabc6f40e446585"
}
import {
to = aws_s3_bucket.logs
id = "demo-logs"
}
to— 가져온 자원을 붙일 코드 주소입니다. 공식 문서는 이 주소가 이미 있는resource블록의 주소와 맞아야 한다고 적습니다(4절의 길). 블록이 없으면 테라폼에게 뽑아 달라고 할 수 있습니다(5절의 길)id— 클라우드가 매긴 그 자원의 ID 입니다. 보안 그룹은sg-…, 인스턴스는i-…, 버킷은 이름입니다
공식 문서(Import a single resource)는 import 를 적용한 결과를 이렇게 설명합니다. 테라폼은 자원을 가져왔고 자신이 만든 것이 아니라는 사실을 기록하고("records that it imported the resources and that it did not create them"), 자원의 속성을 전부 상태 파일로 끌어옵니다("pulls all of the resource's attributes into the state file"). 실제 자원에는 손을 대지 않습니다.
flowchart TD
C["코드<br/>resource aws_security_group.app"] -->|"import 블록 to"| S["상태 파일<br/>aws_security_group.app = sg-48742ecc501d16f90"]
R["실제 자원<br/>sg-48742ecc501d16f90"] -->|"import 블록 id"| S
S --> P["다음 plan 부터<br/>코드 ↔ 실제를 비교"]
그런데 짝을 적는 순간 비교가 시작된다
여기가 이 편의 함정입니다. 짝이 적히면 다음 plan 부터 테라폼은 코드와 실제를 비교해서 다른 곳을 코드 쪽으로 맞추려 합니다. 테라폼은 코드를 정답으로 보는 선언적 설정 도구이기 때문입니다. 코드가 실제와 조금이라도 다르면, import 는 「가져오기」에서 끝나지 않고 「가져와서 코드대로 고치기」가 됩니다. 머리말의 경고가 그렇게 나왔습니다.
4. 첫째 길 — 직접 쓴 코드로 가져오기
1회차 — 설명 하나가 교체를 불렀다
이 시리즈 01편 「손으로 만든 서버는 무엇을 잃나」에서 쓴 짧은 코드를 그대로 가져왔습니다. 손으로 만든 자원을 기억에 기대어 코드로 옮기면 대개 이런 모양이 됩니다.
resource "aws_security_group" "app" {
name = "demo-sg"
description = "demo app sg" # 실제와 다름
ingress { # 22 하나뿐
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
} # 태그 없음
resource "aws_instance" "app" {
ami = "ami-0f47531f8c49bd1c6"
instance_type = "t4g.small"
vpc_security_group_ids = [aws_security_group.app.id]
tags = {
Name = "demo-app"
}
}
# aws_s3_bucket.logs 는 bucket = "demo-logs" 한 줄
ingress { … }— 들어오는 트래픽 규칙 하나입니다.egress는 나가는 트래픽 규칙입니다aws_security_group.app.id— 다른 블록의 값을 참조하는 표현입니다. 「app 보안 그룹의 ID」가 들어갑니다
실제와 다른 곳이 셋입니다. 설명 문구가 다릅니다. 443 규칙과 Name 태그는 빠졌습니다. 여기에 3절의 import 블록 셋을 붙여 plan 을 돌렸습니다.
실측 — 직접 쓴 코드 + import, 1회차 plan (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform plan -no-color
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
…
# aws_instance.app will be updated in-place
# (imported from "i-feaabc6f40e446585")
~ resource "aws_instance" "app" {
…
~ vpc_security_group_ids = [
- "sg-48742ecc501d16f90",
] -> (known after apply)
…
# aws_s3_bucket.logs will be imported
…
# aws_security_group.app must be replaced
# (imported from "sg-48742ecc501d16f90")
# Warning: this will destroy the imported resource
-/+ resource "aws_security_group" "app" {
…
~ description = "launch-wizard-1 created 2026-07-01" -> "demo app sg" # forces replacement
~ egress = [
- {
…
] -> (known after apply)
~ id = "sg-48742ecc501d16f90" -> (known after apply)
~ ingress = [
{
…
from_port = 22
…
},
- {
…
- from_port = 443
…
},
]
…
- tags = {
- "Name" = "demo-sg"
} -> null
…
}
Plan: 3 to import, 1 to add, 1 to change, 1 to destroy.
기호부터 읽겠습니다. 계획 머리에 뜻이 적혀 있습니다. ~ 는 지우지 않고 그대로 고치기(update in-place)이고, -/+ 는 지운 다음 새로 만들기(destroy and then create replacement)입니다.
나머지 표시와 마지막 줄의 수도 여기서 읽힙니다. 버킷처럼 기호 없이 will be imported 만 붙은 자원은 상태 파일에 짝만 적고 아무것도 바꾸지 않습니다. -> (known after apply) 는 적용해 봐야 정해지는 값이라는 뜻입니다. 보안 그룹 id 가 그렇게 찍힌 것은 새 보안 그룹이 생긴다는 뜻입니다. 마지막 줄에서 -/+ 하나는 새로 만들기 1(1 to add)과 지우기 1(1 to destroy)로 세입니다. ~ 인 인스턴스가 1 to change 이고, 3 to import 는 세 자원 모두에 짝을 적는다는 뜻입니다.
왜 고치기가 아니라 지우기일까요. description 줄 끝의 # forces replacement 가 답입니다. aws 프로바이더 문서는 보안 그룹 설명을 「Forces new resource」로 분류합니다. 같은 문서는 AWS 에 설명을 바꾸는 API 가 없다고도 적습니다. 바꿀 수 없는 값이 코드와 다르면 테라폼이 할 수 있는 일은 지우고 다시 만드는 것뿐입니다. 이 판단은 프로바이더가 속성마다 정해 둔 규칙에서 나오는 것이라 moto 가 아닌 진짜 AWS 에서도 같은 계획이 나온다고 봅니다.
이 계획이 하려는 일
계획에 적힌 것을 운영 중인 서버의 말로 옮기면 이렇습니다. 아래는 계획이 하겠다고 한 일이지, 적용한 결과가 아닙니다.
| 계획의 줄 | 계획의 뜻 |
|---|---|
-/+ resource "aws_security_group" · id = "sg-48742ecc501d16f90" -> (known after apply) |
몇 달째 쓰던 보안 그룹을 지우고 ID 가 다른 새 보안 그룹을 만든다 |
- from_port = 443 |
새 보안 그룹에는 443 인바운드 허용이 없다 |
vpc_security_group_ids … -> (known after apply) |
인스턴스를 새 보안 그룹으로 갈아 끼운다 |
- tags … "Name" = "demo-sg" |
콘솔에서 이름으로 찾던 태그를 없앤다 |
egress … -> (known after apply) |
나가기 규칙을 새로 정한다. 프로바이더 문서에 따르면 테라폼은 VPC 안에 새 보안 그룹을 만들 때 AWS 가 기본으로 넣는 「모두 허용」 나가기 규칙을 지운다. 코드에 egress 가 없으니 나가는 트래픽이 막힌다 |
1회차 계획의 인스턴스 변경도 인스턴스 코드가 틀려서가 아니었습니다. 보안 그룹 ID 가 바뀌니 그 ID 를 참조하는 인스턴스도 함께 바뀌는 것입니다.
표는 계획이 끝까지 갔을 때의 모습입니다. 진짜 AWS 에서는 끝까지 가지 못할 가능성이 큽니다. 프로바이더 문서는 인스턴스에 연결된 보안 그룹은 AWS 가 삭제를 거부한다고 적습니다. 또 기본 순서(먼저 지우고 나중에 만들기)로는 그 삭제가 성공하지 않는다고 적습니다. 그러면 apply 는 삭제 타임아웃(기본 15분)까지 기다리다 오류로 끝납니다. 이 계획은 moto 에서도 적용하지 않았습니다.
그래도 이 계획은 위험합니다. 「가져오기」만 하려던 첫 plan 에 운영 자원을 지우려는 계획이 섞여 나왔기 때문입니다. 삭제가 막히면 apply 가 멈춘 채 오류로 끝납니다. 삭제가 통하면 운영 방화벽이 바뀌고 443 과 나가는 트래픽이 막힙니다. 어느 쪽이든 사고입니다.
그래서 import 계획에서 먼저 볼 것은 마지막 줄입니다. 0 to add, 0 to change, 0 to destroy 가 아니면 아직 가져오기가 끝나지 않은 것입니다. 특히 to destroy 가 0이 아니면 절대 apply 하면 안 됩니다.
2회차 — 실제 값에 코드를 맞춘다
1회차 계획이 알려 준 차이 셋을 코드 쪽에서 실제에 맞췄습니다. 설명은 실제 문구로 바꾸고, 443 규칙과 Name 태그를 더했습니다. 바꾼 곳만 보면 이렇습니다.
resource "aws_security_group" "app" {
name = "demo-sg"
# 실제 문구 그대로. 바꾸면 교체된다
description = "launch-wizard-1 created 2026-07-01"
ingress { # 22 뒤에 더함
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { # 더함
Name = "demo-sg"
}
}
실측 — 2회차 plan, 적용, 3회차 plan (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform plan -no-color
terraform apply -auto-approve -no-color
terraform plan -no-color
# aws_instance.app will be imported
…
# aws_s3_bucket.logs will be imported
…
# aws_security_group.app will be imported
…
Plan: 3 to import, 0 to add, 0 to change, 0 to destroy.
Apply complete! Resources: 3 imported, 0 added, 0 changed, 0 destroyed.
### 3회차 plan
No changes. Your infrastructure matches the configuration.
2회차 계획은 3 to import 만 남고 나머지가 전부 0입니다. 이제 이 계획의 일은 상태 파일에 짝 셋을 적는 것뿐입니다. 적용한 뒤 3회차 plan 이 No changes 를 냈습니다. 적용 전 plan 2번 + 확인 1번이었고, 인스턴스 코드는 한 줄도 고치지 않았습니다.
적용 뒤 상태 파일에 남은 것
저장된 상태 파일을 jq(JSON 을 걸러 읽는 명령)로 열어 코드 주소와 실제 ID 만 뽑았습니다.
실측 — import 적용 뒤 상태 파일 (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
jq -r '.resources[]
| "\(.type).\(.name) id=\(.instances[0].attributes.id)"' \
terraform.tfstate
aws_instance.app id=i-feaabc6f40e446585
aws_s3_bucket.logs id=demo-logs
aws_security_group.app id=sg-48742ecc501d16f90
세 줄 다 손으로 만든 자원의 원래 ID 그대로입니다. 새로 만든 것은 없습니다. 이제 테라폼은 이 셋을 「내가 관리하는 것」으로 압니다. 자원이 상태 파일에 남아 있는 한 다음 plan 은 다시 가져오려 하지 않으므로, 적용이 끝난 import 블록은 지워도 됩니다.
적지 않은 egress 는 왜 차이가 안 났나
이상한 점이 하나 남습니다. 코드에 egress 는 끝까지 한 줄도 적지 않았습니다. 그런데 2회차부터 egress 차이가 없었습니다. 반대로 ingress 는 22 하나만 적었더니 적지 않은 443 을 지우려 했습니다.
aws 프로바이더 문서가 이 둘을 설명합니다. 보안 그룹의 ingress·egress 인자를 코드에서 아예 빼면 테라폼은 그 규칙들을 지우지 않습니다. 그래서 한 번도 적지 않은 egress 는 「관리하지 않음」으로 취급됩니다. 반면 ingress 블록을 하나라도 적으면 그것이 ingress 전체 목록으로 읽힙니다. 적지 않은 443 은 「없어야 할 규칙」이 됩니다.
적용 뒤 상태 파일에서 보안 그룹의 egress 만 뽑아 보면, 코드에 없는 규칙이 실제 값 그대로 들어가 있습니다.
실측 — 상태 파일의 egress (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
jq -c '.resources[] | select(.type=="aws_security_group")
| .instances[0].attributes.egress[] | {cidr_blocks, protocol}' \
terraform.tfstate
{"cidr_blocks":["0.0.0.0/0"],"protocol":"-1"}
protocol "-1" 은 모든 프로토콜이라는 뜻입니다. 곧 「모두 허용」 나가기 규칙입니다.
다만 이것은 이미 있는 보안 그룹을 가져왔을 때의 이야기입니다. 4절 표에서 본 대로, 테라폼은 새로 만드는 보안 그룹의 기본 「모두 허용」 나가기 규칙을 지웁니다. 그래서 새로 만들 때 egress 를 안 적으면 나가는 트래픽이 막힙니다.
- 이미 있는 보안 그룹 — 안 적은 규칙 목록에는 손대지 않습니다. 하나라도 적으면 전부 적어야 합니다
- 새로 만드는 보안 그룹 — 안 적은 egress 는 비어 있게 되어 나가는 트래픽이 막힙니다
5. 둘째 길 — 테라폼에게 코드를 뽑게 하기
이 실측은 두 길을 비교하려고, 첫째 길과 다른 폴더의 빈 상태 파일로 같은 자원 셋을 한 번 더 가져왔습니다. 공식 문서(terraform import 명령 설명)는 실제 자원 하나가 코드 주소 하나에만 묶이기를 기대하며, 같은 자원을 여러 번 가져오면 원치 않는 동작이 날 수 있다고 적습니다.
첫째 길의 1회차가 위험했던 것은 사람이 기억으로 코드를 썼기 때문입니다. 그렇다면 코드를 아예 실제에서 받아 적게 하면 어떨까요. resource 블록은 한 줄도 쓰지 않고 import 블록 셋만 둔 채 이렇게 돌립니다.
실측 — 코드 생성, 1회차 (2026-09-30, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform plan -no-color -generate-config-out=generated.tf
-generate-config-out=generated.tf— import 블록이 가리키는데resource블록이 없는 자원에 대해, 테라폼이 실제 값을 읽어 코드 초안을 이 파일에 씁니다. 공식 문서는 이 초안을 각 인자에 대한 테라폼의 「best guess」로 만든 틀(template)이라 부릅니다. 이미 있는 파일 이름을 주면 오류가 납니다
…
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
Warning: Config generation is experimental
Generating configuration during import is currently experimental, and the
generated configuration format may change in future versions.
Error: Conflicting configuration arguments
with aws_instance.app,
…
"primary_network_interface": conflicts with associate_public_ip_address
Error: Conflicting configuration arguments
with aws_instance.app,
…
"ipv6_address_count": conflicts with ipv6_addresses
Error: Conflicting configuration arguments
with aws_instance.app,
…
"ipv6_addresses": conflicts with ipv6_address_count
마지막 줄이 2 to import 인 것은 인스턴스 블록이 오류라서 인스턴스가 가져오기에서 빠졌기 때문입니다. 계획은 오류로 끝났지만 generated.tf 는 120줄로 만들어졌습니다. 공식 문서도 이런 오류가 나도 파일은 쓴다고 적습니다. 보안 그룹과 버킷 블록은 멀쩡했습니다. 문제는 인스턴스 블록이었습니다.
# __generated__ by Terraform
resource "aws_instance" "app" {
ami = "ami-0f47531f8c49bd1c6"
associate_public_ip_address = true
availability_zone = "us-east-1a"
…
ipv6_address_count = 0
ipv6_addresses = []
…
private_ip = "10.79.182.134"
…
source_dest_check = true
subnet_id = "subnet-92c2a3c9c639ef991"
…
vpc_security_group_ids = ["sg-48742ecc501d16f90"]
…
primary_network_interface {
network_interface_id = "eni-2ac6066052312d6d0"
}
}
테라폼은 「지금 있는 값」을 전부 받아 적습니다. 그런데 AWS 가 알아서 채운 값(공인 IP 자동 배정 여부, IPv6 개수와 목록, 네트워크 인터페이스)을 함께 적으면, 둘 중 하나만 적을 수 있다는 프로바이더 규칙(conflicts with)에 걸립니다. ipv6_address_count 와 ipv6_addresses 의 충돌은 공식 문서가 예로 드는 바로 그 경우입니다. 문서가 권하는 해법도 둘 중 하나를 지우고 plan 을 다시 돌리는 것입니다. primary_network_interface 충돌이 moto 탓인지는 가르지 않았습니다.
2회차 — 치우니까 새 충돌이 나왔다
충돌한 세 줄(associate_public_ip_address·ipv6_address_count·ipv6_addresses)을 지우고 다시 plan 했습니다. 이번에는 generated.tf 가 이미 있으니 플래그 없이 terraform plan -no-color 로 돌렸습니다. 3회차도 같습니다.
Error: Conflicting configuration arguments
with aws_instance.app,
…
"primary_network_interface": conflicts with private_ip
1회차에 안 보이던 충돌입니다. 테라폼은 충돌을 한 번에 다 알려 주지 않았습니다. 앞 충돌을 치워야 다음 충돌이 보였습니다.
3회차에는 primary_network_interface { … } 블록을 지우고 다시 plan 했습니다. 이어서 적용하고 확인 plan 을 돌렸습니다.
aws_s3_bucket.logs: Preparing import... [id=demo-logs]
…
Plan: 3 to import, 0 to add, 0 to change, 0 to destroy.
aws_security_group.app: Importing... [id=sg-48742ecc501d16f90]
aws_security_group.app: Import complete [id=sg-48742ecc501d16f90]
…
Apply complete! Resources: 3 imported, 0 added, 0 changed, 0 destroyed.
### 그다음 plan
No changes. Your infrastructure matches the configuration.
적용 전 plan 3번(앞의 두 번은 오류) + 확인 1번이었습니다. 지운 곳 넷(속성 세 줄과 블록 하나)으로 오류 없는 계획이 나왔고, 적용 뒤 확인 plan 이 No changes 였습니다. 첫째 길과 달리 교체 계획은 한 번도 나오지 않았습니다. 설명 문구도 443 규칙도 태그도 실제에서 받아 적었으니 어긋날 것이 없었습니다.
뽑힌 코드는 값이 하드코딩돼 있다
대신 남은 코드를 보면 문제가 다른 쪽에 있습니다. 사설 IP 10.79.182.134, 서브넷 ID, 보안 그룹 ID sg-48742ecc501d16f90 이 문자 그대로 박혀 있습니다. 첫째 길의 코드는 aws_security_group.app.id 로 참조했는데, 여기서는 ID 가 글자로 굳었습니다. 값이 하드코딩된 코드라 같은 구성을 하나 더(시험용 환경 등) 만드는 데 쓸 수 없습니다. 생성된 파일 머리의 주석 # Please review these resources and move them into your main configuration files. 가 그 말을 합니다.
공식 문서도 생성된 코드에서 출발해 속성을 지우고, 값을 고치고, 블록을 옮기면서 원하는 코드로 다듬어 가라고 권합니다. 그 다듬기가 한 번 끝날 때마다 다시 plan 을 돌려 No changes 인지 확인해야 합니다. 결국 둘째 길도 첫째 길의 반복으로 이어집니다. 또 -generate-config-out 은 테라폼 스스로 「experimental」이라 경고하는 기능이라 버전에 따라 출력 모양이 바뀔 수 있습니다.
6. 맞추기는 반복이다
두 길 모두 같은 과정을 되풀이했습니다. plan 이 차이를 보여 주면, 코드를 실제 쪽에 맞추고, 다시 plan 합니다.
flowchart TD
A["import 블록 + 코드"] --> B["terraform plan"]
B --> C{"마지막 줄이<br/>N to import, 0 to add,<br/>0 to change, 0 to destroy ?"}
C -->|"아니다<br/>(오류 · ~ · -/+)"| D["차이 난 값을<br/>실제 값으로 코드에 적는다"]
D --> B
C -->|"그렇다"| E["terraform apply<br/>상태 파일에 짝을 적는다"]
E --> F["terraform plan<br/>No changes"]
| 첫째 길 — 직접 쓴 코드 | 둘째 길 — -generate-config-out |
|
|---|---|---|
| plan 횟수 | 적용 전 2번 + 확인 1번 | 적용 전 3번(앞 두 번은 오류) + 확인 1번 |
| 1회차에 나온 것 | 교체 계획 -/+ 과 1 to destroy |
인스턴스 속성 충돌 오류 3건 |
| 고친 것 | 설명 · 443 규칙 · 태그를 더함 | 충돌 오류 4건을 풀려고 속성 3줄 · 블록 1개를 지움 |
| 남은 코드 | 짧고 참조로 이어짐. 다시 쓸 수 있음 | 120줄에서 114줄. ID·IP 가 하드코딩됨 |
| 위험 | 1회차 계획이 운영 보안 그룹을 지우려 함 | 오류로 멈추니 모르고 지울 일은 없었음 |
두 길의 성격이 다릅니다. 직접 쓴 코드는 모양이 좋지만 기억이 틀린 곳에서 교체 계획이 튀어나옵니다. 뽑힌 코드는 실제와 어긋나지 않지만 값이 하드코딩돼 다시 쓸 수 없습니다. 어느 길이든 끝은 같은 판정입니다. 적용 전 계획의 마지막 줄이 import 수 말고는 전부 0이어야 합니다.
맞추는 방향은 코드 쪽이다
이 반복에서 고치는 것은 언제나 코드입니다. 실제 보안 그룹에는 SSH(22)가 전 세계에 열려 있습니다. 두 길 모두 그 규칙을 코드에 그대로 옮겨 적었습니다. import 는 손으로 만든 서버의 잘못된 설정까지 그대로 가져옵니다.
그 잘못된 설정을 고치는 것은 No changes 에 닿은 다음 일입니다. 먼저 코드가 실제를 정확히 비추게 한 뒤, 22 규칙을 좁히는 변경을 따로 만들어 plan 으로 확인하고 적용합니다. 가져오기와 고치기를 한 계획에 섞으면, 머리말의 경고처럼 무엇이 가져오기 때문이고 무엇이 고치기 때문인지 계획을 읽어도 가를 수 없게 됩니다.
한 장 요약
- 테라폼은 상태 파일에 적힌 짝만 믿으므로, 실제와 똑같은 코드도 import 없이 apply 하면
Plan: 3 to add로 새로 만들러 갑니다. import { to = 코드 주소, id = 실제 ID }는 실제 자원을 건드리지 않고 상태 파일에 짝을 적는 일입니다.- 짝이 적히면 코드가 정답이 되어, 직접 쓴 코드의 1회차 plan 은 설명 문자열 하나(
# forces replacement) 때문에 운영 보안 그룹을 지우고 다시 만들겠다고 했습니다. 진짜 AWS 에서는 연결된 보안 그룹 삭제가 막혀 apply 가 멈출 가능성이 크지만, 막히든 통하든 사고입니다. - 직접 쓴 코드는 교체 계획이, 뽑힌 코드는 충돌 오류와 하드코딩된 값이 문제였습니다. 두 길의 plan 횟수는 6절 표와 같습니다.
- 보안 그룹 규칙 목록은 이미 있는 것을 가져올 때 안 적으면 손대지 않지만, 새로 만들 때는 안 적은 egress 가 비어 나가는 트래픽이 막힙니다.
- 적용 전 계획의 마지막 줄이
N to import, 0 to add, 0 to change, 0 to destroy가 될 때까지 코드를 실제에 맞추고, 열린 SSH 같은 잘못된 설정은 그다음 별도 변경으로 고칩니다.
관련 항목
import 가 기대는 개념
테라폼 · 상태 파일 · HCL · 선언적 설정 · 멱등성 · 코드형 인프라
import 로 옮겨 오는 대상
스노우플레이크 서버 · 보안 그룹 · EC2 · S3 · AWS 01-1 서버를 통째로 빌린다 — EC2·Lightsail·Beanstalk·CloudShell
import 와 함께 쓰는 테라폼 도구
terraform plan · terraform apply · terraform import · terraform state · moved 블록 · removed 블록
import 뒤에 맞서는 문제
구성 드리프트 · 자원 교체 · 원격 상태 · 상태 잠금
같은 일을 하는 다른 도구
CloudFormation · 앤서블 · Terraformer · AWS 08 빌드·배포 자동화 — ECR·CodeBuild·CodePipeline·CodeDeploy·CloudFormation·CDK
IaC 시리즈의 다른 편
IAC 01 손으로 만든 서버는 무엇을 잃나 — 명령형 스크립트와 선언형 코드 · IAC 02 plan 이 말해 주는 것 — 제자리 변경·교체·상태 파일 · IAC 04 누가 콘솔에서 손대면 — 드리프트와 테라폼이 못 보는 것 · IAC 05 둘이 같이 apply 하면 — 원격 상태와 잠금