LayerX エンジニアブログ

LayerX の エンジニアブログです。

Terraform import のススメ 〜開発効率化編〜

こんにちは、LayerX で主にインフラを担当している高江です。
今回は、一見地味ではありますが実はとても役に立つ機能である Terraform import についてお話したいと思います。 

Terraform import とは

公式サイトでは次のように説明されています。

Terraform is able to import existing infrastructure. This allows you take resources you've created by some other means and bring it under Terraform management.

要するに、AWS 等のサービスプロバイダー上に既に存在する、Terraform 管理されていないリソースの情報を取得して Terraform 管理下に置く(tfstate ファイルに import する)機能です。
また、同じく公式サイトには

This is a great way to slowly transition infrastructure to Terraform,

ともあります。
このことから、まだ IaC (Infrastructure as Code) を実践していない環境において、既存のリソースを運用しながら徐々に Terraform による IaC を進めていくときなどに有用な機能であることが分かります。

ただ、import の利用シーンはそれだけではなく、開発を効率化する際にも力を発揮します。
ここでは、普段の開発において私がどのように import を活用しているかをご紹介します。

Terraform import の使い方

その前に、まずは import の使い方について簡単に説明します。
コマンドの形式は以下になります。 (公式サイト参照)

% terraform import [options] ADDRESS TO

ADDRESS は Terraform ファイル(.tf)内で定義した、対象リソースの type と name です。 TO は対象とするリソースによって違いがあり、ID だったり ARN だったりします。

また、import を実行するためには、事前に対象のリソースを定義した Terraform ファイルを作成しておく必要があります。 例えば、sample-bucket という名前の S3 bucket を import する場合の手順は以下のとおりです。

1.Terraform ファイルを作成

resource "aws_s3_bucket" "sample_bucket" {
    bucket = "sample-bucket"
}

2.Terraform import を実行

% terraform import aws_s3_bucket.sample_bucket sample-bucket

これによって、import した sample-bucket が tfstate に追加され、Terraform 管理下に置かれることになります。
S3 bucket を import する場合は上記のように bucket 名を指定します。対象リソースによって何を指定するかは変わってくるので、詳細は公式サイトの各リソースのページを参照ください。ページ最下部に import する場合の説明が記載されています。S3 bucket の場合はコチラになります。

Terraform import を使って開発を効率化する

ここからが本題です。Terraform を使ってインフラを構築するにあたり、「この場合どう書けばいいんだっけ?」というケースが出てくると思います。特に、初めて Terraform で構築しようとするリソースであったり、Terraform を使った開発経験自体があまりない場合には、正しいリソース定義の書き方について迷うことも多いのではないでしょうか。

このような場合に、公式サイトのドキュメントを見たりググったりしながら手探りで作るのではなく、import を利用することで開発を効率化することができます。
ここでは、AWS CodePipeline と AWS CodeBuild を使ったビルドパイプラインを構築する例をもとに、具体的な開発の流れについて説明します。

パイプラインの概要

構築するパイプラインを図1に示します。

f:id:shnjtk:20210625102609p:plain
図1 ビルドパイプライン アーキテクチャ

  • CodePipeline でパイプライン全体の構成を管理
  • CodeStar connections で GitHub リポジトリと接続
  • 取得したコードを CodeBuild に渡すために S3 bucket をアーティファクトストアとして利用
  • CodeBuild でビルド実行
  • IAM で各種権限管理

開発の流れ

では開発に移っていきましょう。全体の流れとしては以下のようになります。

  1. AWS 管理画面からマニュアルでリソースを構築
  2. terraform import で tfstate を更新
  3. tfstate の内容を見ながら Terraform ファイルを編集
  4. terraform plan で差分を確認しながら Terraform ファイルを編集

1. AWS 管理画面からマニュアルでリソースを構築

AWS 管理画面にログインし、ボタンポチポチでパイプラインを構築していきます。このとき、マニュアルで作るものは仮として、後から Terraform で作り直す場合はそれと分かるようにリソース名の prefix や suffix に temp のようなものを付与することをおすすめします。今回は、リソース名を terraform-import-sample としました。

f:id:shnjtk:20210625112144p:plain
図2 AWS 管理画面からのパイプライン構築の様子

2. terraform import で tfstate を更新

次に、以下の Terraform ファイルを作成して import を実行します。

  • main.tf
terraform {
  required_version = "~> 1.0"

  required_providers {
    aws = {
      version = "~> 3.47"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_codepipeline" "import_sample" {}
  • コマンド
% terraform init

# AWS CodePipeline の import はリソース名を指定する
% terraform import aws_codepipeline.import_sample terraform-import-sample

そうすると、次のような tfstate ファイルが作成されます。これが、Terraform ファイルでリソースを定義していく上でのヒントになります。

{
  "version": 4,
  "terraform_version": "1.0.1",
  "serial": 1,
  "lineage": "00c448d1-bda9-2c60-68e8-0be02331c6d9",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_codepipeline",
      "name": "import_sample",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:codepipeline:ap-northeast-1:(snip):terraform-import-sample",
            "artifact_store": [
              {
                "encryption_key": [],
                "location": "codepipeline-ap-northeast-1-(snip)",
                "region": "",
                "type": "S3"
              }
            ],
            "id": "terraform-import-sample",
            "name": "terraform-import-sample",
            "role_arn": "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "stage": [
              {
                "action": [
                  {
                    "category": "Source",
                    "configuration": {
                      "BranchName": "main",
                      "ConnectionArn": "arn:aws:codestar-connections:ap-northeast-1:(snip):connection/(snip)",
                      "FullRepositoryId": "(snip)",
                      "OutputArtifactFormat": "CODE_ZIP"
                    },
                    "input_artifacts": [],
                    "name": "Source",
                    "namespace": "SourceVariables",
                    "output_artifacts": [
                      "SourceArtifact"
                    ],
                    "owner": "AWS",
                    "provider": "CodeStarSourceConnection",
                    "region": "ap-northeast-1",
                    "role_arn": "",
                    "run_order": 1,
                    "version": "1"
                  }
                ],
                "name": "Source"
              },
              {
                "action": [
                  {
                    "category": "Build",
                    "configuration": {
                      "ProjectName": "terraform-import-sample"
                    },
                    "input_artifacts": [
                      "SourceArtifact"
                    ],
                    "name": "Build",
                    "namespace": "BuildVariables",
                    "output_artifacts": [
                      "BuildArtifact"
                    ],
                    "owner": "AWS",
                    "provider": "CodeBuild",
                    "region": "ap-northeast-1",
                    "role_arn": "",
                    "run_order": 1,
                    "version": "1"
                  }
                ],
                "name": "Build"
              }
            ],
            "tags": {},
            "tags_all": {}
          },
          "sensitive_attributes": [],
          "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
        }
      ]
    }
  ]
}

3. tfstate の内容を見ながら Terraform ファイルを編集

main.tf を編集していきます。tfstate ファイルや公式サイトのCodePipeline ドキュメントを参照しながら、必要なパラメータを設定します。

  • main.tf (terraformproviderは省略)
resource "aws_codepipeline" "import_sample" {
  name     = "terraform-import-sample"
  role_arn = "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp"

  artifact_store {
    type     = "S3"
    location = "codepipeline-ap-northeast-1-(snip)"
  }

  stage {
    name = "Source"
    action {
      category         = "Source"
      name             = "Source"
      namespace        = "SourceVariables"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]
      configuration = {
        ConnectionArn        = "arn:aws:codestar-connections:ap-northeast-1:(snip):connection/(snip)"
        FullRepositoryId     = "(snip)"
        BranchName           = "main"
        OutputArtifactFormat = "CODE_ZIP"
      }
    }
  }

  stage {
    name = "Build"
    action {
      category         = "Build"
      name             = "Build"
      namespace        = "BuildVariables"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      run_order        = 1
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact"]
      configuration = {
        ProjectName : "terraform-import-sample"
      }
    }
  }
}

4. terraform plan で差分を確認しながら Terraform ファイルを編集

terraform plan を実行してみましょう。

% terraform plan
aws_codepipeline.import_sample: Refreshing state... [id=terraform-import-sample]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

変更なしと表示されましたね。これで、マニュアルで作ったものと main.tf ファイルで定義したものが同一になることが確認できました。

CodePipeline 以外のリソースについても同様の作業を行います。例えば、今の main.tf では IAM role が ARN ハードコードになっているので、これを Terraform のリソースとして定義します。 そのため、次はこの IAM role を import します。

  • main.tf (IAM role 定義部分のみ抜粋)
resource "aws_iam_role" "import_sample_codepipeline" {}
  • コマンド
# IAM role の import はリソース名を指定する
% terraform import aws_iam_role.import_sample_codepipeline AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp

そうすると、tfstate に IAM role が追加されます。(該当箇所のみ抜粋)

    {
      "mode": "managed",
      "type": "aws_iam_role",
      "name": "import_sample_codepipeline",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codepipeline.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
            "create_date": "2021-06-25T01:22:35Z",
            "description": "",
            "force_detach_policies": false,
            "id": "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "inline_policy": [
              {
                "name": "",
                "policy": ""
              }
            ],
            "managed_policy_arns": [
              "arn:aws:iam::(snip):policy/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-sample"
            ],
            "max_session_duration": 3600,
            "name": "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "name_prefix": null,
            "path": "/service-role/",
            "permissions_boundary": null,
            "tags": {},
            "tags_all": {},
            "unique_id": "AROAZYMBT63SJTFIQE7EF"
          },
          "sensitive_attributes": [],
          "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
        }
      ]
    }

これを参考に、CodePipeline の時と同じようにリソースを定義します。(該当箇所のみ抜粋)

resource "aws_codepipeline" "import_sample" {
    role_arn = aws_iam_role.import_sample_codepipeline.arn
}

resource "aws_iam_role" "import_sample_codepipeline" {
  name = "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp"
  path = "/service-role/"

  assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codepipeline.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
}

terraform plan を実行してみましょう。

% terraform plan
aws_iam_role.import_sample_codepipeline: Refreshing state... [id=AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp]
aws_codepipeline.import_sample: Refreshing state... [id=terraform-import-sample]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

今回も変更なしと表示されましたね。上手くいきました。

このように、import した tfstate ファイルを見ながら必要なパラメータを設定していくことで Terraform によるインフラ開発を効率化することができます。一通りリソース定義が終わったら、マニュアルで作ったリソースや import した tfstate はそのままにして運用するもよし、一旦削除して各種リソース名等をプロジェクトの命名規則等にあわせて作り直すもよしです。

まとめ

以上、Terraform import を利用したインフラ開発の効率化についてご紹介しました。いかがでしたでしょうか?
Terraform import は既存リソースを Terraform 管理下に置く場合に使うものという意識があるかもしれませんが、新規にリソースを構築する際にもとても有用なので、ぜひご活用いただければと思います。 余談ですが、答えを見ながら必要な項目を埋めていくので若干カンニングしてる感があります。

おまけ

ちなみに、上記の IAM role リソース定義で、 assume_role_policy は string でベタ書きされていますが、明らかにメンテナンス性が低いので、Data Source を使って定義した方がよいです。

こんな感じです。(関連箇所のみ抜粋)

data "aws_iam_policy_document" "import_sample_codepipeline_assume_role_policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["codepipeline.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "import_sample_codepipeline" {
  assume_role_policy = data.aws_iam_policy_document.import_sample_codepipeline_assume_role_policy.json
}

あるいは、Terraform の IAM role の 公式ドキュメントにもあるように、 jsonencode を使ってもいいですね。
一つのリソースでしか使わないのであれば jsonencode でインラインに、複数のリソースで使うのであれば Data Source として再利用できるように、それぞれ使い分ければいいかなと思います。

それでは。