クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Terraformingで既存のIAMユーザをTerraform管理下に入れる

クラウドワークスの缶コーヒーエンジニアの森田(@minamijoyo)です。

だいたい毎日缶コーヒーを飲みながら主にインフラ周りの仕事をしてるので、クラウドワークスのインフラの一部は缶コーヒーでできていると言っても過言ではないんじゃなかろうかと思う今日この頃。ちなみに最近のマイブームはFIREのハワイアン微糖です。

これまでのあらすじ

クラウドワークスではAWS(Amazon Web Services)でサーバを運用しており、AWSでのIAM(Identity and Access Management)ユーザのグループポリシーの管理について、以前こんな記事を書きました。

TerraformでAWSのIAMユーザのグループポリシーを管理する - Qiita

Terraform+Atlas+GitHubでAWSのIAMユーザのグループポリシーをいいかんじに管理する - クラウドワークス エンジニアブログ

上記の記事では、IAMのグループポリシーの部分をTerraformで管理する方法について説明していますが、Terraformそのものは既に存在しているAWSリソースは管理できないため、既存のIAMユーザそのものについては、一旦Terraformの管理対象外としていました。

この記事で書くこと

しかしながら、IAMユーザがTerraform管理外だと片手落ちです。というわけで、Terraformingという既存のAWSリソースをTerrafromに変換するツールを使って、既存のIAMユーザをTerraform管理下に入れる方法について説明します。この記事では Terraform v0.6.12、Terraforming v0.7.0を使用しています。

ポイントは以下のとおりです。

  • Terraformはリソースの状態をtfstateファイルに保存している。tfstateはただのJSONなので手動で編集可能。(お約束ですが自己責任で)
  • Terraform自体には既存のAWSリソースを管理する機能はないが、Terraformingという非公式ツールがあるのでこれを使おう。
  • Terraformingは非公式ツールだけど、生成したファイルをマージしてterraform planして差分がなければTerraformとして問題ないことは確認できるので互換性はそこで担保。
  • Terraformingで自動生成されるのそのまま使っても良いけど、tfstateの構造が分かれば必要なところだけ手動でリソース名をリネームしたり個別調整することも可能。

Terraformとtfstate

Terraformでは、*.tf ファイルにリソースを定義して terraform apply コマンドでAWSなどのクラウドリソースに反映します。*.tf ファイルのリソース定義を修正した場合は terraform plan でリソースへの変更差分を確認することができます。この前回からの変更差分をどうやって検出しているかというと、 デフォルトではローカルのカレントディレクトリの.terraform/terraform.tfstate というファイルにリソース状態を保持しています。 この .tfstate ファイルはテキストエディタで開くとわかりますが、ただのJSONファイルです。

例えば以下のような main.tf というファイルがあった場合、

resource "aws_iam_group" "group1" {
    name = "group1"
}

以下のような .terraform/terraform.tfstate ファイルが生成されています。

{
    "version": 1,
    "serial": 38,
    "remote": {
        "type": "s3",
        "config": {
            "bucket": "xxxx",
            "key": "xxxx"
        }
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_iam_group.group1": {
                    "type": "aws_iam_group",
                    "primary": {
                        "id": "group1",
                        "attributes": {
                            "arn": "arn:aws:iam::xxxx:group/group1",
                            "id": "group1",
                            "name": "group1",
                            "path": "/",
                            "unique_id": "xxxx"
                        }
                    }
                },
                "aws_iam_group.xxxx": {

...(以下、Terraformのリソースが順に出力される)

resourcesのところに、Terraformが認識しているリソースの情報が並んでます。これを見るとTerraformがどのようにリソースを認識しているかが分かってなかなか面白いです。

tfstateファイルの複数人での共有

余談ですが、tfstateを複数人で共有する場合は、これをAtlasやS3などに保存して共有します。 以前紹介したHashiCorp公式のAtlasはベータ期間が終了して有料化されたようです。(既存ユーザは2016/3/8から6ヶ月間の猶予期間が与えられています。)

New Interface Design, User Experience, and Pricing for Atlas - HashiCorp

他のHashiCorpプロダクトと合わせて使うならAtlasを使うメリットがあると思いますが、ただのtfstate置き場だけであればS3でも十分かもしれません。tfstateをS3に置く方法は以下が参考になります。

Amazon S3 で Terraform の状態管理ファイル terraform.tfstate を管理 / 共有する - Qiita

Terraformig とは

tfstateの構造を覗いたら簡単に推測できると思いますが、当然ながらTerraformでは自分で作ったリソースは管理できますが、既に存在するリソースを管理することはできません。既存リソースも管理したいという需要は一定存在すると思いますが、今のところTerraformの公式機能としては存在せず、非公式にTerraformingというツールが開発されているので、これを使ってみます。

github.com

Terraformingは現状実質的にAWS用ですが、既存のAWSリソースの情報を取得して、.tf ファイルと .tfstate ファイルを生成してくれます。

Terraformは活発に開発されているので、非公式ツールを使うと互換性が気になるところですが、最終的に生成したファイルをマージして terraform plan して差分がなければTerraformとして問題ないことは確認できるので、そこで何かを壊してないかは担保できるんじゃなかろうかと思います。あと、TerraformingはAWSのリソースの種類ごとにサブコマンドが分かれているので、必要な部分だけ少しずつTerraformingしていくという作戦も可能です。tfstateのdiffが目視で確認レベルであれば最終的になんとでもなるはず。

Terraformingのインストール

それではTerraformingをインストールしてみましょう。 Terraformを既に使っていればAWSのアクセスキーなどの設定はできていると思うのでそのへんは割愛します。 TerraformingはRubyのgemとして公開されているのでgemコマンドでインストールします。

$ gem install terraforming
$ gem list | grep terraforming
terraforming (0.7.0)

terraforming help してサブコマンドの一覧が表示されればインストールできてます。

$ terraforming help
Commands:
  terraforming asg             # AutoScaling Group
  terraforming dbpg            # Database Parameter Group
  terraforming dbsg            # Database Security Group
  terraforming dbsn            # Database Subnet Group
  terraforming ec2             # EC2
  terraforming ecc             # ElastiCache Cluster
  terraforming ecsn            # ElastiCache Subnet Group
  terraforming eip             # EIP
  terraforming elb             # ELB
  terraforming help [COMMAND]  # Describe available commands or one specific command
  terraforming iamg            # IAM Group
  terraforming iamgm           # IAM Group Membership
  terraforming iamgp           # IAM Group Policy
  terraforming iamip           # IAM Instance Profile
  terraforming iamp            # IAM Policy
  terraforming iamr            # IAM Role
  terraforming iamrp           # IAM Role Policy
  terraforming iamu            # IAM User
  terraforming iamup           # IAM User Policy
  terraforming igw             # Internet Gateway
  terraforming nacl            # Network ACL
  terraforming nif             # Network Interface
  terraforming r53r            # Route53 Record
  terraforming r53z            # Route53 Hosted Zone
  terraforming rds             # RDS
  terraforming rs              # Redshift
  terraforming rt              # Route Table
  terraforming rta             # Route Table Association
  terraforming s3              # S3
  terraforming sg              # Security Group
  terraforming sn              # Subnet
  terraforming vpc             # VPC

Options:
  [--merge=MERGE]                  # tfstate file to merge
  [--overwrite], [--no-overwrite]  # Overwrite existng tfstate
  [--tfstate], [--no-tfstate]      # Generate tfstate
  [--profile=PROFILE]              # AWS credentials profile

helpの出力を見るとサブコマンドはAWSのリソースごとに分かれているようです。

既存のIAMユーザをterraformingする

これ以降、tfstateファイルを色々いじるので、壊してしまった場合に備えて .terraform/*.tfstate を適宜バックアップしておいて下さい。

$ cp -p .terraform/*.tfstate (backup_dir)/

それでは本題のIAMユーザのterraformingをしましょう。 IAMユーザ用のサブコマンドはiamuです。

terraforming iamu コマンドを実行するとこんなかんじで標準出力に .tf 形式でaws_iam_userリソースのTerraformコードが生成されます。

$ terraforming iamu
resource "aws_iam_user" "MasayukiMORITA" {
    name = "MasayukiMORITA"
    path = "/"
}

resource "aws_iam_user" "xxxx" {
    name = "xxxx"
    path = "/"
}

....(以下、ユーザが順に出力される)

ユーザ数が多いと大量に出力されるので、ファイルにリダイレクトしておきます。

$ terraforming iamu > aws_iam_user.tf

次にterraformingコマンドに --tfstate オプションを付けて、tfstateファイルを作ります。

$ terraforming iamu --tfstate
{
  "version": 1,
  "serial": 1,
  "modules": [
    {
      "path": [
        "root"
      ],
      "outputs": {
      },
      "resources": {
        "aws_iam_user.MasayukiMORITA": {
          "type": "aws_iam_user",
          "primary": {
            "id": "MasayukiMORITA",
            "attributes": {
              "arn": "arn:aws:iam::xxxx:user/MasayukiMORITA",
              "id": "MasayukiMORITA",
              "name": "MasayukiMORITA",
              "path": "/",
              "unique_id": "xxxx"
            }
          }
        },
        "aws_iam_user.xxxxx": {

...(以下、ユーザが順に出力される)

このまま必要な部分を既存のtfstateファイルに手動で貼り付けてマージしてもよいのですが、terraformingコマンドに --merge オプションがあり、既存のtfstateとマージした出力を生成できます。

$ terraforming iamu --tfstate --merge=./.terraform/terraform.tfstate > ./.terraform/terraform.tfstate.merged

ちなみにterraformingには --overwrite というオプションもあるようなのですが、いきなり上書きされるのはちょっと怖いので、適当なファイルにリダイレクトしてdiffしてみて、どういう変更が入るかを確認した方が無難かと思います。diffは空白の出力の仕方が微妙に違って差分があるので -b で空白差分を無視してます。

$ diff -bu .terraform/terraform.tfstate .terraform/terraform.tfstate.merged
--- .terraform/terraform.tfstate        2016-05-25 13:39:27.000000000 +0900
+++ .terraform/terraform.tfstate.merged 2016-05-25 13:57:24.000000000 +0900
@@ -1,6 +1,6 @@
 {
     "version": 1,
-    "serial": 38,
+  "serial": 39,
     "remote": {
         "config": {
@@ -12,7 +12,8 @@
             "path": [
                 "root"
             ],
-            "outputs": {},
+      "outputs": {
+      },
             "resources": {
@@ -376,6 +377,838 @@
+        },
+        "aws_iam_user.MasayukiMORITA": {
+          "type": "aws_iam_user",
+          "primary": {
+            "id": "MasayukiMORITA",
+            "attributes": {
+              "arn": "arn:aws:iam::xxxx:user/MasayukiMORITA",
+              "id": "MasayukiMORITA",
+              "name": "MasayukiMORITA",
+              "path": "/",
+              "unique_id": "xxxx"
+            }
+          }
+        },
+        "aws_iam_user.xxxx": {

...

mergeオプションで出力するとserialが1増えてます。このserialはtfstateが更新される度にインクリメントされる値で、複数人でtfsateを共有する場合に、この数値によってリモートとローカルでどちらが最新かを判断するのに使用されています。

diffの差分が問題なさそうであればであれば .terraform/terraform.tfstate で上書きして、 terraform plan を実行してみます。

$ cp .terraform/terraform.tfstate.merged .terraform/terraform.tfstate
$ terraform plan

planの差分がなければ、既存のリソースをTerraform管理下に置くことができたといえます。

Terraformのリソース名を手動で調整したい

ここからは必須ではないですが、ネーミングルールなどを調整します。 Terrafomingで生成されたtfファイルですが、Terraformのリソース名にはAWSリソース名がそのまま使われています。

$ terraforming iamu
resource "aws_iam_user" "MasayukiMORITA" {
    name = "MasayukiMORITA"
    path = "/"
}

しかしながら、既存のTerraformリソースは公式マニュアル同様にスネークケースなネーミングにしていたのでこんなかんじにしたかったのです。

$ terraforming iamu
resource "aws_iam_user" "masayuki_morita" {
    name = "MasayukiMORITA"
    path = "/"
}

とりあえず上記のようにtfファイルを修正しますが、tfファイルを変更すると、対応するtfstateの記述も調整する必要があります。 resourcesのところを aws_iam_user.MasayukiMORITA から aws_iam_user.masayuki_morita に手動で編集して、serialをインクリメントして保存します。

{
    "version": 1,
    "serial": 40,
    "remote": {
        "type": "s3",
        "config": {
            "bucket": "xxxx",
            "key": "xxxx"
        }
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_iam_user.masayuki_morita": {
                    "type": "aws_iam_user",
                    "primary": {
                        "id": "MasayukiMORITA",
                        "attributes": {
                            "arn": "arn:aws:iam::xxxx:user/MasayukiMORITA",
                            "id": "MasayukiMORITA",
                            "name": "MasayukiMORITA",
                            "path": "/",
                            "unique_id": "xxxx"
                        }
                    }
                },
                "aws_iam_user.xxxx": {

...

※id、arn、nameの箇所はAWS上のリソース名なのでリネームしません。

terraform plan を実行して差分が検出されなければOKです。

$ terraform plan

IAMグループメンバ定義からIAMユーザのリソースを参照する

これも必須ではないですが、IAMユーザもTerraform管理下になったのでIAMグループメンバの定義もIAMユーザを参照するように書き換えて明示的な依存を付けておきましょう。 依存があればリソースの変更時にTerraformが正しく変更順序の依存を解決してくれます。

resource "aws_iam_group" "group1" {
    name = "group1"
}

resource "aws_iam_group_membership" "group1_membership" {
    name = "group1"
    group = "${aws_iam_group.group1.name}"
    users = [
        "${aws_iam_user.masayuki_morita.name}",
        "${aws_iam_user.xxxx.name}",
        ...
    ]
}

...

以上で既存のIAMユーザをTerraform管理下に入れることができました。 要領が分かれば、IAMユーザ以外でも既存のAWSリソースをTerraform化できそうですね。

まとめ

Terraformingを使うことで既存のIAMユーザもTerraform管理下に置くことができました。最近は新規に作るAWSリソースは基本的にTerraformに寄せていってますが、既存リソースについても徐々にコード化していくつもりです。クラウドソーシングのクラウドワークス では、インフラもコードで書きたくて仕方がないエンジニアを募集しています。

© 2016 CrowdWorks, Inc., All rights reserved.