Skip to content
ioob.dev
Go back

Terraform 7편 — 데이터 소스와 Import

· 10분 읽기
Terraform 시리즈 (7/15)
  1. Terraform 1편 — Terraform이란
  2. Terraform 2편 — 설치와 첫 배포
  3. Terraform 3편 — HCL 문법
  4. Terraform 4편 — 변수와 출력
  5. Terraform 5편 — 프로바이더
  6. Terraform 6편 — 리소스와 의존성
  7. Terraform 7편 — 데이터 소스와 Import
  8. Terraform 8편 — State 관리
  9. Terraform 9편 — 모듈
  10. Terraform 10편 — 반복과 조건
  11. Terraform 11편 — 워크스페이스와 환경 분리
  12. Terraform 12편 — Kubernetes와 Helm 프로바이더
  13. Terraform 13편 — CI/CD 통합
  14. Terraform 14편 — 테스트와 정책
  15. Terraform 15편 — 실전 패턴과 함정
Table of contents

Table of contents

세상은 이미 Terraform 바깥에 있다

지금까지의 시리즈는 “빈 클라우드에 Terraform으로 인프라를 만든다”는 전제에서 흘러왔다. 하지만 실무는 다르다. 회사에 들어가면 콘솔로 만든 VPC가 이미 있고, 누군가 수동으로 만든 S3 버킷이 3년째 돌고 있으며, 네트워킹은 다른 팀이 관리하는 별도 Terraform 프로젝트에 들어있다. 새 시스템을 만들어도 그 기존 자산과 연결돼야 한다.

이 편은 두 가지 도구를 다룬다.

둘의 차이는 “소유권”에 있다. 데이터 소스는 “나는 안 만들었고 안 지워도 돼, 값만 알려줘”이고, import는 “이제부터 내가 관리할게”다.

flowchart LR
    subgraph EXIST[이미 존재하는 리소스]
        R[콘솔로 만든 VPC<br/>다른 팀 관리]
    end

    subgraph ME[내 Terraform]
        D[data 블록<br/>읽기만]
        I[terraform import<br/>관리로 편입]
    end

    R -->|조회| D
    R -->|편입| I
    D -->|속성 사용| USE1[내 리소스]
    I -->|향후 Terraform 관리| TF[tfstate에 기록]

data 블록 — 조회 전용 리소스

data는 겉보기엔 resource와 닮았다. 레이블 두 개(타입과 이름)를 받고, 인자로 조건을 준다. 다른 점은 Terraform이 생성/수정/삭제를 하지 않는다는 것이다. terraform apply 할 때 AWS API를 호출해서 해당 조건에 맞는 리소스를 찾아 속성을 가져올 뿐이다.

AMI 조회 — 가장 흔한 패턴

2편에서 잠깐 봤던 패턴이다.

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
}

AMI ID를 "ami-0c55b159cbfafe1f0"처럼 하드코딩하지 않고, “Amazon이 게시한 al2023 중 최신”을 런타임에 찾아온다. AMI가 새로 나오면 자동으로 따라간다. 참조 문법은 리소스와 비슷하지만 data. 접두사가 붙는다는 점이 다르다.

기본 VPC/서브넷 조회

각 리전에 AWS가 자동으로 만들어주는 기본 VPC가 있다. 간단한 실습에선 이걸 그대로 쓰고 싶다.

data "aws_vpc" "default" {
  default = true
}

data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
  subnet_id     = tolist(data.aws_subnets.default.ids)[0]
}

aws_subnets(복수형)는 리스트를 돌려주므로, tolist(...)[0]로 첫 번째 서브넷을 뽑는다. 이런 식으로 기본 네트워크 위에 리소스를 얹을 수 있다.

다른 팀이 관리하는 리소스 참조

실무에서 더 중요한 케이스다. 네트워크 팀이 “VPC는 우리가 Terraform으로 관리하고, 각 팀은 그 위에 서비스를 올린다”라는 구조를 취한다고 하자. 서비스 팀은 VPC ID가 필요하지만 관리는 하지 않는다.

방법은 여러 가지다.

# 1. 태그로 찾기
data "aws_vpc" "platform" {
  tags = {
    Name = "platform-main"
    Env  = "prod"
  }
}

# 2. ID를 변수로 받기
variable "vpc_id" {
  type = string
}

data "aws_vpc" "platform" {
  id = var.vpc_id
}

# 3. 다른 Terraform State에서 output 읽기 (remote state)
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "myorg-terraform-state"
    key    = "network/prod/terraform.tfstate"
    region = "ap-northeast-2"
  }
}

# 사용
resource "aws_subnet" "my_service" {
  vpc_id     = data.terraform_remote_state.network.outputs.vpc_id
  cidr_block = "10.0.100.0/24"
}

3번 방식(terraform_remote_state)은 네트워크 팀이 output으로 노출한 값을 우리 프로젝트에서 바로 읽는다. 깔끔하지만 강한 결합이 생긴다(원격 상태의 output 구조가 바뀌면 내 코드도 깨진다). 조직이 커지면 “네트워크 정보 조회 전용 data source”를 따로 만들어두거나(예: aws_ssm_parameter에 값을 저장), 태그 기반 조회로 느슨하게 결합하는 편이 유지보수에 좋다.

계정 정보, 리전, 가용 영역

자주 쓰이는 메타 정보도 데이터 소스로 조회한다.

data "aws_caller_identity" "current" {}

data "aws_region" "current" {}

data "aws_availability_zones" "available" {
  state = "available"
}

locals {
  account_id = data.aws_caller_identity.current.account_id
  region     = data.aws_region.current.name
  azs        = data.aws_availability_zones.available.names
}

resource "aws_s3_bucket" "logs" {
  # 계정 ID와 리전이 포함된 유일한 이름
  bucket = "myapp-logs-${local.account_id}-${local.region}"
}

계정 ID를 하드코딩하는 실수를 막을 수 있고, 여러 리전에 같은 코드를 재사용하기도 쉬워진다.

data와 resource의 차이 정리

flowchart TB
    subgraph RES[resource]
        RC[Create]
        RU[Update]
        RD[Delete]
        RS[Read]
    end

    subgraph DATA[data]
        DS2[Read 전용]
    end

    APPLY[terraform apply] --> RC
    APPLY --> RU
    APPLY --> RD
    APPLY --> RS
    APPLY --> DS2

    DESTROY[terraform destroy] -.->|영향| RD
    DESTROY -.->|영향 없음| DS2

data는 apply 때마다 실제 클라우드의 현재 값을 다시 조회한다. 캐시되지 않는다. 이 성질 때문에 네트워크 호출이 많아지는 경우가 있으니, 수십 개의 data 블록이 같은 리소스를 중복 조회하고 있으면 한 번만 조회해서 locals에 담는 리팩토링을 고려한다.

terraform import — 기존 리소스를 편입

이번엔 반대 방향이다. 콘솔로 만든 S3 버킷이나 RDS 같은 “Terraform이 모르는” 리소스를 Terraform 관리 안으로 가져온다.

import의 목적은 분명하다. 기존 리소스를 재생성하지 않고, State에 등록해서 이후부터 Terraform이 관리하게 한다. 운영 중인 DB를 지웠다 새로 만들면 재앙이니, 이런 방식이 꼭 필요하다.

예전 방식 (CLI 명령)

Terraform 1.4 이전 버전까지는 CLI 명령으로 진행했다. 지금도 동작하는 고전 방식이다.

# 1. .tf 파일에 빈 리소스 선언
cat > imported.tf <<EOF
resource "aws_s3_bucket" "legacy_logs" {
  bucket = "legacy-logs-bucket"
}
EOF

# 2. import 명령으로 State에 편입
terraform import aws_s3_bucket.legacy_logs legacy-logs-bucket

# 3. plan으로 현재 설정과 실제 상태의 차이 확인
terraform plan

import 명령의 형식은 terraform import <리소스_주소> <리소스_ID>다. 리소스 ID의 형태는 리소스 타입마다 다르다(EC2는 i-xxx, S3는 버킷 이름, RDS는 identifier). 각 리소스의 공식 문서 하단에 “Import” 섹션이 있으니 거기서 확인한다.

문제는 이 방식이 State만 바꾸고 코드는 건드리지 않는다는 점이다. 가져온 뒤 terraform plan을 돌리면 “내 .tf 파일에는 bucket 이름만 있는데, 실제 버킷엔 버저닝, 암호화, CORS 설정이 다 있어요”라며 엄청난 diff가 뜬다. 이걸 하나씩 코드로 옮기는 작업이 굉장히 지루했다.

선언형 import 블록 (Terraform 1.5+)

그래서 1.5부터 import 블록이 추가됐다. 선언형 방식에 맞게 “내가 이 리소스를 이 주소로 가져온다”를 코드에 쓴다.

# imports.tf
import {
  to = aws_s3_bucket.legacy_logs
  id = "legacy-logs-bucket"
}

# 여전히 리소스 블록은 필요
resource "aws_s3_bucket" "legacy_logs" {
  bucket = "legacy-logs-bucket"
  # ... 나머지 설정
}

이 상태에서 terraform plan을 돌리면 Terraform이 “이 리소스를 import할 계획이고, 현재 .tf와 실제 상태 간 차이는 이렇다”를 한 번에 보여준다. apply하면 import까지 자동으로 된다.

더 좋은 건 코드 생성 옵션이다.

terraform plan -generate-config-out=imported.tf

Terraform이 실제 리소스의 속성을 조사해서 .tf 코드를 자동으로 생성해준다. 생성된 코드를 읽으면서 필요한 부분만 남기고 정리한다. 지루한 옮겨적기가 크게 줄어든다.

flowchart LR
    subgraph OLD[예전 방식]
        C1[빈 resource 작성] --> C2[terraform import]
        C2 --> C3[plan으로 diff 확인]
        C3 --> C4[코드를 실제와 맞게 수정]
    end

    subgraph NEW[1.5+ import 블록]
        N1[import 블록 작성] --> N2["terraform plan<br/>-generate-config-out=..."]
        N2 --> N3[자동 생성된 코드<br/>정리]
        N3 --> N4[terraform apply]
    end

신규 프로젝트라면 1.5+ 방식을 쓴다. 레거시 import 문서도 여전히 돌아가긴 하지만, 선언형 import가 훨씬 Terraform스럽다.

import 할 때 주의할 점

import는 State만 채워주기 때문에, 몇 가지 함정이 있다.

  1. 코드와 실제의 diff를 주의깊게 본다. 한 번에 수십 개의 속성을 바꾸는 plan이 잡히면, 실제로 적용했을 때 뭐가 재생성될지 예측이 어렵다. -/+가 보이면 그건 의도치 않은 재생성이다. 이런 건 코드를 실제에 맞게 먼저 고친 뒤 apply해야 한다
  2. 태그가 어긋나는 경우가 많다. default_tags를 Provider에 걸어뒀다면, 기존 리소스엔 그게 빠져 있어서 전부 “태그 추가” 변경으로 잡힌다. 무해한 변경이지만 plan이 지저분해진다
  3. 민감한 리소스는 prevent_destroy를 먼저 걸어둔다. import 직후 실수로 destroy가 돌면 재앙이다. 운영 DB를 import할 거라면 lifecycle { prevent_destroy = true }를 리소스에 붙이고 시작한다
  4. 모듈 내부 리소스 import는 주소를 정확히. module.foo.aws_instance.bar 같은 형태로 쓴다

moved — 리팩토링 시 리소스 주소 변경

데이터 소스와 import 얘기에 가까운 또 하나의 도구가 moved 블록이다. Terraform 1.1부터 추가됐다. 리소스를 재생성하지 않고 “내부 주소만 바꾸고 싶을 때” 쓴다.

실무에서 자주 일어나는 상황이다. 리팩토링하다가 리소스를 모듈로 빼는 경우.

# 예전 구조
resource "aws_s3_bucket" "logs" {
  bucket = "myapp-logs"
}

# 새 구조 — 모듈로 이동
module "logs" {
  source = "./modules/bucket"
  name   = "myapp-logs"
}

이렇게 바꾸면 Terraform은 aws_s3_bucket.logs를 삭제하고, module.logs.aws_s3_bucket.this를 새로 만든다. 실제 버킷이 한 번 지워졌다가 다시 생기는 것이다. 운영 환경에선 감당할 수 없는 변경이다.

해결책은 moved 블록.

moved {
  from = aws_s3_bucket.logs
  to   = module.logs.aws_s3_bucket.this
}

이 선언이 있으면 Terraform은 “내부적으로 State의 주소를 바꾼다”고 인식한다. 실제 리소스는 건드리지 않는다. 리팩토링이 안전해진다.

예전에는 이걸 terraform state mv 같은 CLI 명령으로 했는데, 명령형이라 재현성이 없었다. moved 블록은 선언형이고 Git에 남으니 팀원도 그 기록을 볼 수 있다.

콘솔 리소스를 Terraform으로 옮기는 실전 흐름

이 편의 도구들을 종합해서, 회사에 “콘솔로 만든 RDS가 있는데 이제 Terraform으로 관리하고 싶다”는 과제가 왔다고 해보자. 실전 단계를 그려보면 이렇다.

sequenceDiagram
    participant Dev as 개발자
    participant TF as Terraform
    participant AWS as AWS

    Dev->>AWS: 현재 RDS 설정 조사 (콘솔, CLI)
    Dev->>TF: import 블록 + resource 작성
    Note over TF: lifecycle { prevent_destroy = true }
    Dev->>TF: terraform plan -generate-config-out
    TF-->>Dev: 자동 생성된 코드
    Dev->>TF: 코드 정리 & 검토
    Dev->>TF: terraform plan (최종)
    TF-->>Dev: No changes / 안전한 변경만
    Dev->>TF: terraform apply
    TF->>AWS: State에 RDS 등록
    Note over Dev,AWS: 이후부터 Terraform이 관리

단계를 말로 풀면 이렇다.

  1. 조사: 실제 RDS의 엔진, 버전, 스토리지, VPC, 보안 그룹, 파라미터 그룹을 모두 파악한다
  2. 작성: .tf에 대응하는 aws_db_instance를 적고, import 블록을 추가한다. 맨 앞에 prevent_destroy를 붙인다
  3. 자동 생성: terraform plan -generate-config-out=...으로 현재 상태를 코드화한다
  4. 정리: 생성된 코드에서 Terraform이 관리하기 어려운 속성(status처럼 AWS가 정하는 값)을 제거하고, 관리하지 않을 속성은 ignore_changes에 넣는다
  5. 검증: terraform plan이 “No changes” 혹은 허용 가능한 변경만 보이는지 확인
  6. apply: 이 시점부터 Terraform이 주도권을 갖는다

겉으론 복잡해 보이지만, 1.5+ 방식은 예전에 비해 훨씬 덜 고통스럽다. 회사에 콘솔 유산이 많다면 이 과정을 팀 작업으로 진행할 가치가 있다.

조회와 편입, 언제 무엇을 쓰나

마지막으로 선택 기준을 정리하자.

이 세 가지 도구가 조합되면, 완전히 새로 만든 인프라가 아니어도 Terraform의 이점을 점진적으로 누릴 수 있다. 레거시와 신규 코드가 공존하는 환경에서 특히 가치가 크다.

기초 파트를 지나며

7편에 걸쳐 Terraform의 기초를 훑었다. IaC의 개념에서 시작해 설치, HCL 문법, 변수, 프로바이더, 리소스, 그리고 기존 인프라와의 연결까지 왔다. 여기까지 왔다면 일반적인 AWS/GCP 인프라 프로젝트 코드를 읽고 수정하고, 새 기능을 추가하는 건 어렵지 않다.

8편부터는 실전 운영 주제로 들어간다. State 관리, 모듈, 반복 구조, 환경 분리, CI/CD 통합, 테스트와 정책까지 Terraform의 규모 있는 활용을 다룬다.


다음 편에서는 Terraform State를 원격에 저장하고 팀 단위로 안전하게 공유하는 방법을 다룬다.

8편: State 관리


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Terraform 6편 — 리소스와 의존성
Next Post
Terraform 8편 — State 관리