Terraform має два способи перенести існуючі ресурси під управління Terraform – з Terraform CLI і командою
terraform import
, або використовуючи ресурс import
.
Для чого нам може знадобитись імпорт ресурсів?
- якщо у нас вже є вручну налаштований (“clickops”) якийсь сервіс, який ми хочемо перенести під управління Terraform (робили як Proof of Concept, а потім пішло в production)
- якщо у нас є ресурси, які створювались з іншою IaC системою, наприклад – з CloudFormation
- якщо ми втратили наш state-файл, і треба його відновити
- чи якщо ми розбиваємо один великий проект на менші і створюємо нові state-файли
Окрім Terraform CLI та import
block є інструменти по типу Terraformer та Terracognita, які частину роботи роблять самі – але сьогодні ми все спробуємо без них.
Зміст
Процес імпорту ресурсів
Як виглядає процес імпорту:
- в tf-файлі описуємо пустий resource
- виконуємо
terraform import
- порівнюємо дані в state-файлі і нашому коді
- переносимо зміни до tf-файлу
- …
- profit!
Приклад імпорту з Terraform CLI
Створимо AWS IAM User:
$ aws --profile setevoy iam create-user --user-name iam-user-to-be-imported { "User": { "Path": "/", "UserName": "iam-user-to-be-imported", "UserId": "AIDAT3EEMW7XERE75PH6N", "Arn": "arn:aws:iam::264***286:user/iam-user-to-be-imported", "CreateDate": "2025-06-14T12:15:52+00:00" } }
Створюємо тестовий Terraform проект:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { profile = "setevoy" region = "us-east-1" }
Виконуємо terraform init
:
$ terraform init Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 5.0"... - Installing hashicorp/aws v5.100.0... - Installed hashicorp/aws v5.100.0 (signed by HashiCorp) ...
Імпорт AWS IAM User
Описуємо блок для IAM-юзеру, якого ми будемо переносити під управління Terraform. В цьому випадку це буде aws_iam_user
:
... resource "aws_iam_user" "imported_iam_user" { # Import this user using the command: # terraform import aws_iam_user.imported_iam_user iam-user-to-be-imported name = "iam-user-to-be-imported" }
Про параметри, які треба вказувати в нашому “шаблоні”:
- якщо б це був, наприклад, S3-бакет – то для нього всі параметри опціональні, і достатньо було просто указати
resource "aws_s3_bucket" "example" {}
- але для aws_iam_user є обов’язковий параметр
name
Тому задаємо required параметр name
– цього досить.
Тепер можемо виконати сам імпорт юзеру з AWS вказавши ім’я ресурсу в коді (його ідентифікатор для terraform) – aws_iam_user.imported_iam_user
та ім’я юзера в AWS IAM:
$ terraform import aws_iam_user.imported_iam_user iam-user-to-be-imported aws_iam_user.imported_iam_user: Importing from ID "iam-user-to-be-imported"... aws_iam_user.imported_iam_user: Import prepared! Prepared aws_iam_user for import aws_iam_user.imported_iam_user: Refreshing state... [id=iam-user-to-be-imported] Import successful!
Перевіряємо Terraform state:
$ terraform state list aws_iam_user.imported_iam_user
І зміст об’єкту aws_iam_user.imported_iam_user
в цьому стейті:
$ terraform state show aws_iam_user.imported_iam_user # aws_iam_user.imported_iam_user: resource "aws_iam_user" "imported_iam_user" { arn = "arn:aws:iam::264***286:user/iam-user-to-be-imported" id = "iam-user-to-be-imported" name = "iam-user-to-be-imported" path = "/" permissions_boundary = null tags = {} tags_all = {} unique_id = "AIDAT3EEMW7XERE75PH6N" }
Далі можемо оновлювати наш код, але є нюанс.
No changes. Your infrastructure matches the configuration.
Тепер цікавий момент: якщо зараз виконати terraform plan
– то Terraform скаже, що ніяких змін робити не потрібно:
$ terraform plan aws_iam_user.imported_iam_user: Refreshing state... [id=iam-user-to-be-imported] No changes. Your infrastructure matches the configuration.
Хоча, здавалося б – в нашому коді не описані ніякі атрибути юзера, які ми бачимо в terraform state show
.
Причина в тому, що коли ми створювали IAM-юзера з AWS CLI та командою create-user
– то не вказували ніяких додаткових опцій, і AWS CLI через AWS API створив його з усіма дефолтними параметрами.
Аналогічно робить і Terraform, коли ми запускаємо terraform plan
– він перевіряє значення в AWS зі значеннями, які описані в провайдері, бачить, що все дефолтне – і тому каже, що ніяких змін робити не треба.
Як приклад – дефолтне значення path
задається в провайдері явно як "/"
– див. internal/service/iam/user.go#L62
:
... names.AttrPath: { Type: schema.TypeString, Optional: true, Default: "/", }, ...
Тепер давайте створимо нового юзера, але вже з явно заданим path
:
$ aws --profile setevoy iam create-user --user-name iam-user-to-be-imported-2 --path /some-path/
Додаємо його в код як і першого юзера – без додаткових параметрів:
... resource "aws_iam_user" "imported_iam_user_2" { # Import this user using the command: # terraform import aws_iam_user.imported_iam_user iam-user-to-be-imported name = "iam-user-to-be-imported" }
Тепер виконаємо terraform import
:
$ terraform import aws_iam_user.imported_iam_user_2 iam-user-to-be-imported-2
І ще раз подивимось на результат terraform plan
– то цього разу Terraform захоче змінити атрибут path
юзера:
$ terraform plan ... # aws_iam_user.imported_iam_user_2 will be updated in-place ~ resource "aws_iam_user" "imported_iam_user_2" { + force_destroy = false id = "iam-user-to-be-imported-2" name = "iam-user-to-be-imported-2" ~ path = "/some-path/" -> "/" tags = {} # (4 unchanged attributes hidden) }
Оновлення main.tf
Окей, йдемо далі.
Імпорт ми зробили – юзер у нас є в state-файлі, і є код в main.tf
– “пустий шаблон” цього юзера.
Аби завершити імпорт – нам потрібно привести наш код до такого ж стану, який є в state, бо наш код має бути source of truth для цього ресурсу.
При цьому нам не потрібно переносити абсолютно всі параметри зі стейту Terraform до ресурсу в коді: ми переносимо тільки те, чим хочемо явно керувати, або якщо ми хочемо їх відобразити в коді для його ясності.
Є параметри, для яких задаються дефолтні значення, є параметри, які генеруються самим AWS.
- конфігураційні для Terraform resource: ім’я,
path
,tags
тощо- для більшості є дефолтні значення
- generated параметри:
arn
,unique_id
(UserId
в outputs AWS CLI)
Конфігураційні параметри ми бачимо в документації до ресурсу в Argument Reference.
Параметри, які генерує сам AWS описані в Attribute Reference.
Інший спосіб це визначити – зазирнути в код провайдера, наприклад для unique_id
значення вказано в internal/service/iam/user.go#L82
:
... "unique_id": { Type: schema.TypeString, Computed: true, }, ...
Тут в полі Computed
ми як раз і бачимо, що воно визначається автоматично з AWS.
Отже, в цьому випадку нам точно треба задати path
:
... resource "aws_iam_user" "imported_iam_user_2" { name = "iam-user-to-be-imported-2" path = "/some-path/" }
Виконуємо terraform plan
– і тепер ніяких змін нема:
$ terraform plan aws_iam_user.imported_iam_user_2: Refreshing state... [id=iam-user-to-be-imported-2] aws_iam_user.imported_iam_user: Refreshing state... [id=iam-user-to-be-imported] No changes. Your infrastructure matches the configuration.
Terraform diffing mechanism
Тепер ще один цікавий момент.
Якщо ми задамо якісь додаткові атрибути нашому вже імпортованому юзеру, наприклад теги:
resource "aws_iam_user" "imported_iam_user_2" { name = "iam-user-to-be-imported-2" path = "/some-path/" tags = { ManagedBy = "Terraform" } }
І виконаємо terraform plan
ще раз:
$ terraform plan ... Terraform will perform the following actions: # aws_iam_user.imported_iam_user_2 will be updated in-place ~ resource "aws_iam_user" "imported_iam_user_2" { + force_destroy = false id = "iam-user-to-be-imported-2" name = "iam-user-to-be-imported-2" ~ tags = { + "ManagedBy" = "Terraform" } ...
То побачимо цікаву річ: на цей раз Terraform хоче додати атрибут force_destroy = false
.
Чому це?
Бо force_destroy
– це атрибут, який існує тільки в коді самого Terraform, але його немає в AWS: під час виконання terraform import
– Terraform з AWS API отримав ті атрибути, які надає AWS, і зберіг їх у своєму state.
Відповідно зараз в state нема force_destroy
:
$ terraform state show aws_iam_user.imported_iam_user_2 # aws_iam_user.imported_iam_user_2: resource "aws_iam_user" "imported_iam_user_2" { arn = "arn:aws:iam::264***286:user/some-path/iam-user-to-be-imported-2" id = "iam-user-to-be-imported-2" name = "iam-user-to-be-imported-2" path = "/some-path/" permissions_boundary = null tags = {} tags_all = {} unique_id = "AIDAT3EEMW7XER5THQH4Q" }
Коли Terraform виконує plan
– то в першому випадку, коли ми задали тільки значення name
та path
:
- під час виконання
plan
– Terraform виконує “швидку перевірку” – порівнює аргументи в коді з даними в state - бачить, що ніяких змін не було – і на цьому завершує роботу з повідомленням “Your infrastructure matches the configuration“
Другий випадок – ми додали tags
, і тоді:
- під час виконання
plan
– Terraform виконує “швидку перевірку” – порівнює аргументи в коді з даними в state - Terraform бачить, що деякі атрибути ресурсу змінились – і починає виконувати більш детальну перевірку формуючи повну схему ресурсу з усіма дефолтними значенням
- Terraform бачить, що в state-файлі нема аргументу
force_destroy
, і планує додати його до state
Власне, оскільки force_destroy
у нас і так має дефолтне значення false, то ми просто можемо виконати terraform apply
, після чого в state з’явиться новий атрибут:
$ terraform state show aws_iam_user.imported_iam_user_2 # aws_iam_user.imported_iam_user_2: resource "aws_iam_user" "imported_iam_user_2" { arn = "arn:aws:iam::264***286:user/some-path/iam-user-to-be-imported-2" force_destroy = false ... permissions_boundary = null tags = { "ManagedBy" = "Terraform" } ...
Імпорт в модуль
Окрім того, що ми можемо імпортувати об’єкти як звичайні Terraform resources, ми можемо їх додавати в модулі.
Наприклад, модуль Anton Babenko terraform-aws-modules/iam, в якому є сабмодуль iam-user.
Створимо “шаблон” в нашому main.tf
:
... module "iam_user_imported" { source = "terraform-aws-modules/iam/aws//modules/iam-user" name = "iam-user-to-be-imported-2" path = "/some-path/" }
Дивимось, як заданий ресурс в самому модулі – файл modules/iam-user/main.tf
:
resource "aws_iam_user" "this" { count = var.create_user ? 1 : 0 name = var.name path = var.path force_destroy = var.force_destroy permissions_boundary = var.permissions_boundary tags = var.tags }
Виконуємо terraform init
, і можемо імпортувати нашого юзера використовуючи ідентифікатор module.iam_user_imported.aws_iam_user.this
.
Але.
Помилка “Configuration for import target does not exist”
Запускаємо імпорт – і отримуємо помилку:
$ terraform import module.iam_user_imported.aws_iam_user.this iam-user-to-be-imported-2 ╷ │ Error: Configuration for import target does not exist │ │ The configuration for the given import module.iam_user_imported.aws_iam_user.this does not exist. All target instances must have an associated configuration to be imported.
Чому?
Бо повернемось до коду модулю:
... count = var.create_user ? 1 : 0 ...
При використанні count
– Terraform створює список з елементами, навіть якщо він там один.
Тобто, в умові сказано: “якщо var.create_user == true, то створюємо один об’єкт” – але це вже буде об’єкт list з одним елементом.
Тому до ресурсу треба звертатись по індексу – [0]
:
$ terraform import module.iam_user_imported.aws_iam_user.this[0] iam-user-to-be-imported-2 module.iam_user_imported.aws_iam_user.this[0]: Importing from ID "iam-user-to-be-imported-2"... module.iam_user_imported.aws_iam_user.this[0]: Import prepared! Prepared aws_iam_user for import module.iam_user_imported.aws_iam_user.this[0]: Refreshing state... [id=iam-user-to-be-imported-2]
І тепер він є в нашому state:
$ terraform state show module.iam_user_imported.aws_iam_user.this[0] # module.iam_user_imported.aws_iam_user.this[0]: resource "aws_iam_user" "this" { arn = "arn:aws:iam::264***286:user/some-path/iam-user-to-be-imported-2" id = "iam-user-to-be-imported-2" name = "iam-user-to-be-imported-2" path = "/some-path/" permissions_boundary = null tags = { "ManagedBy" = "Terraform" } tags_all = { "ManagedBy" = "Terraform" } unique_id = "AIDAT3EEMW7XER5THQH4Q" }
Terraform import
block
І подивимось як працює import
в самому коді.
В принципі, тут все теж саме – вказуємо що (id
) та куди (to
) імпортувати:
import { to = aws_iam_user.imported_iam_user_3 id = "iam-user-to-be-imported-2" } resource "aws_iam_user" "imported_iam_user_3" { name = "iam-user-to-be-imported-2" path = "/some-path/" tags = { ManagedBy = "Terraform" } }
Тепер при виконанні terraform plan
ми будемо бачити що саме і з якими параметрами буде імпортуватись:
$ terraform plan aws_iam_user.imported_iam_user_3: Preparing import... [id=iam-user-to-be-imported-2] aws_iam_user.imported_iam_user_3: Refreshing state... [id=iam-user-to-be-imported-2] aws_iam_user.imported_iam_user: Refreshing state... [id=iam-user-to-be-imported] ... Terraform will perform the following actions: # aws_iam_user.imported_iam_user_3 will be imported resource "aws_iam_user" "imported_iam_user_3" { arn = "arn:aws:iam::264***286:user/some-path/iam-user-to-be-imported-2" id = "iam-user-to-be-imported-2" name = "iam-user-to-be-imported-2" path = "/some-path/" permissions_boundary = null tags = { "ManagedBy" = "Terraform" } tags_all = { "ManagedBy" = "Terraform" } unique_id = "AIDAT3EEMW7XER5THQH4Q" } Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. ...
Готово.