Terraform: використання import, та деякі неочевидні нюанси

Автор |  14/06/2025
 

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.
...

Готово.