CloudFormationでIAM Userをたくさん作る

IAMユーザをたくさん作る機会なんて滅多にないと思います（思いたいです）が、実際そういうケースに遭遇すると滅入ります。
一気に作れないかなぁとおもい、CloudFormationから作ってみました。


# TL;DR

* テンプレートを作ってCloudFormationにアップロードすればあとは待つだけ。1回作っておけば何度もやる場合は楽。
* テンプレートを書くのは結構面倒な上、ユーザのパスワードの生成は自分でやらざるを得ない。
* ユーザへのパスワードの伝達を自動化しないとそこで手間がかかる。
* Stackを削除すると作ったユーザは消える。消えないようにもできる。


# CloudFormationとは
[ここ](https://aws.amazon.com/jp/cloudformation/)を読んでもよくわかりませんが、簡単に言うとシステムの構成を管理するやつです。
プロビジョニングとかオーケストレーションとか呼ばれたりもします。

Stackという単位でシステム構成を定義しておき、それを実行することで定義通りの構成を一気に作ることができます。
また、リソース間の依存関係を理解して順序良くリソースを作成してくれたり、途中で失敗した時にロールバックしてくれたりもします。


# テンプレートを作る
ものすごくシンプルに書くと、だいたい以下の感じになります。

```json:CreateUsers.template
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Metadata": {
  },
  "Resources": {
  	"{{Userリソース名}}": {
  	  "Type": "AWS::IAM::User",
  	  "Properties": {
  	    "UserName": "{{User名}}",
  	    "LoginProfile": {
    	    "Password": "{{パスワード}}",
    	    "PasswordResetRequired": true
    	},
    	"Groups": [
  	      { "Ref": "{{Groupリソース名1}}" },
  	      { "Ref": "{{Groupリソース名2}}" },
  	    ]
      }
  	}
  }
}
```

* *Metadataの中身は割愛しました。*
* *可変部分を**{{ }}**で記載しました。*

| プレイスホルダ | 説明 |
|---|---|
| Userリソース名 | このUserリソースを識別するための名前。 |
| User名 | ログイン時に使用するUser名。 |
| パスワード | ログイン時に使用するパスワード。 |
| Groupリソース名N | このUserが参加するGroupのリソース名。 |

`{{Userリソース名}}: { ... }`の定義を増やしていくことで、Userを増やすことができます。

上記の他、`ManagedPolicyArns`や`Path`、`Policies`などが指定できますが今回は使いません。

ドキュメントは[ここ](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html)。

## ランダムパスワードの生成ができない

IAMのコンソールを使うと、ランダムなパスワードを発行して設定してくれる機能がありますが、CloundFormationからはできません。

## テンプレートを書くのが以外に面倒

あとはコピペしてプレイスホルダを置き換えて...なんですが、これも意外と面倒なのでスクリプトで生成することにしました。
スクリプトはこのエントリの終わりに記載しておきます。

# テンプレートをアップロードしてStackを作る

1. **Create Stack**
![Screen Shot 2016-11-29 at 9.29.10.png](https://qiita-image-store.s3.amazonaws.com/0/76146/f7adcf9f-09be-3897-0fc9-82c9fb95830f.png "Screen Shot 2016-11-29 at 9.29.10.png")

2. **Choose a template** の **Upload a template to Amazon S3** を選択し、作ったテンプレートファイルを選択。
![Screen Shot 2016-11-29 at 9.30.17.png](https://qiita-image-store.s3.amazonaws.com/0/76146/eeaea99d-70ce-f67d-bf95-a675dfb886e4.png "Screen Shot 2016-11-29 at 9.30.17.png")

3. **Stack name** を設定。
![Screen Shot 2016-11-29 at 9.44.24.png](https://qiita-image-store.s3.amazonaws.com/0/76146/4cf8c205-a26c-f3e9-4d5c-e03a875145d1.png "Screen Shot 2016-11-29 at 9.44.24.png")

4. 今回は特に何も指定しなくていいのでそのまま次へ。
![Screen Shot 2016-11-29 at 9.47.17.png](https://qiita-image-store.s3.amazonaws.com/0/76146/e9c3f782-75fb-e827-986e-b67e5519424a.png "Screen Shot 2016-11-29 at 9.47.17.png")

5. **I acknowledge that AWS CloudFormation might create IAM resources with custom names.** にチェック。

# 完了を待つ

Stackを作ったあとそのまま実行されるので、**Status** が **CREATE_COMPLETE** になるまで待ちます。

![Screen Shot 2016-11-29 at 9.54.56.png](https://qiita-image-store.s3.amazonaws.com/0/76146/3c08fd22-85cf-fc27-8482-343c251c9ea6.png "Screen Shot 2016-11-29 at 9.54.56.png")

## 失敗した場合

User名やGroup名がすでに存在するものと被ると失敗します。その場合、勝手にロールバックしてくれますが、Stackは残ってしまうので削除してやりなおしてください。

## 成功した場合

IAMを見ると、

* ユーザが作成されている
* グループに所属している
* パスワードが設定されている

が確認できます。
![Screen Shot 2016-11-29 at 9.59.07.png](https://qiita-image-store.s3.amazonaws.com/0/76146/0da4659e-4b63-05b2-87f6-02969820a0fd.png "Screen Shot 2016-11-29 at 9.59.07.png")

また、CloudFormationに戻って、Designerを開くと構成が確認できます。
![template1-designer.png](https://qiita-image-store.s3.amazonaws.com/0/76146/da4bc35d-1621-2b97-78ea-f52627e9b982.png "template1-designer.png")


# ユーザにログインアカウントを連絡する

現状は、この部分とログインURLをslackか何かで送る感じです。
<small>*ここ自動化しないとだいぶ手間だよなぁ...*</small>

```
  	    "UserName": "{{User名}}",
  	    "LoginProfile": {
    	    "Password": "{{パスワード}}",
    	    "PasswordResetRequired": true
    	  },
```


---

# おまけ
## Stackを削除すると
作ったユーザ、グループは消えます。
作ったユーザ、グループは消えます。
作ったユーザ、グループは消えます。

作成したStackを残して置いてメンテしていくのであれば問題ありませんが、そうでない場合は、 **Deletion Policy** を設定しておけばStackを消してもリソースは残ります。

ドキュメントは[ここ](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html)

## ユーザの定義部分を生成するスクリプト（Node）
```javascript:index.js
const process = require('process');
const args = require('args');
const genpass = require('generate-password');
const upperCamelCase = require('uppercamelcase');


const resourcename = username => {
  return upperCamelCase('user-' + username.replace(/[^a-zA-Z0-9]/, '_'));
}

const groups = list => {
  return list.split(',').map((g) => {
    return {
      "Ref": g
    }
  });
}

const password = () => {
  return genpass.generate({
    length: 16,
    numbers: true,
    symbols: true,
    uppercase: true
  });
}

///

args
  .option('user', 'User ID')
  .option('groups', 'Groups');

const params = args.parse(process.argv);

if (typeof params.user !== 'string') {
  console.error("User IDを指定してください。");
  process.exit(1);
}
if (params.groups != null && typeof params.groups !== 'string') {
  console.error("Group IDを指定してください。");
  process.exit(1);
}


// AWS::IAM::User Resource を作る。
const resname = resourcename(params.user);
const res = {};
res[resname] = {
  Type: "AWS::IAM::User",
  Properties: {
    UserName: params.user,
    LoginProfile: {
      Password: password(),
      PasswordResetRequired: true
    }
  }
};
if (params.groups != null) {
  res[resname].Properties.Groups = groups(params.groups);
}

// 出力
console.log(JSON.stringify(res, null, 2));
```

### 使い方

```sh:例
node index.js -u testuser1 -g AdministratorsGroup,DevelopersGroup
```