Cloud9共有環境に、最小権限でチームメンバーを招待してみた

## はじめに
Cloud9には環境を共有する機能があります。
この機能を利用することでリモートでもペア・モブプロが可能となります。

https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/share-environment.html


この時、共有相手用のIAMが必要となります。

ポリシーは最小権限にせよ、という~~無視しがちな~~ベストプラクティスを守る練習として、この時用意する共有相手用のIAMを最小権限で用意してみたいと思います。

### アーキテクチャ

![cap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/0656da75-e9fe-3e95-231e-f242f9ee20a4.png)

cloud9環境の共有用に、以下のリソースを用意します。
- Cloud9被共有者用ポリシーのみをアタッチしたロール
- 上記ロールへのみスイッチできるユーザーグループ
- 上記ユーザーグループにのみ所属し、パスワード変更以外のallowポリシーを持たない共有相手用のIAM

この時、共有相手のIAMは、Readを含む全てのポリシーを許可されていないため、AWSアカウントが持つすべてのリソースにアクセスできません。
唯一、スイッチロールのみが利用可能で、スイッチロール後は、共有されたCloud9環境での作業のみ行うことができるようになります。

### tfコード
#### ディレクトリ構成
```
.
├── main.tf
├── variable.tf
├── terraform.tfvars
├── cloud9.tf
└── IAM.tf
```

#### variable.tf
``` variable.tf
######################################
## terraform.tfvarsから変数取得
######################################
variable "cloud9_subnet_id" {
  type = string
}

variable "owner_arn" {
  type    = string
  description = "If default, terraform execution arn becomes the owner of the cloud9 instance"
  default = ""
}
```

- Cloud9にはownerというユーザーを指す属性が存在します。
- 招待機能はこのownerしか使用できません。
- デフォルトではownerはリソース作成者（この場合はterraform実行ユーザー）となります。
- ownerをterraform実行ユーザー以外に設定する場合に指定するvarを定義しています。

#### main.tf
``` main.tf
############################################################################
## terraformブロック
############################################################################
terraform {
  # Terraformのバージョン指定
  required_version = "~> 1.7.0"

  # Terraformのaws用ライブラリのバージョン指定
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.33.0"
    }
  }
}

############################################################################
## providerブロック
############################################################################
provider "aws" {
  # リージョンを指定
  region = "ap-northeast-1"
}
```

- Cloud9のimageに「amazonlinux-2023-x86_64」を使いたかったので、terraform、providerのverを最新にしています

#### cloud9.tf
``` cloud9.tf
data "aws_caller_identity" "default" {}

resource "aws_cloud9_environment_ec2" "main" {
  instance_type = "t2.micro"
  name          = "example-env"
  image_id      = "amazonlinux-2023-x86_64"
  subnet_id     = var.cloud9_subnet_id
  owner_arn     = coalesce(var.owner_arn, data.aws_caller_identity.default.arn)
}
```

- 引数のうち、nullやブランクでない最初の値を返却するcoalesce関数を使用しています。
```
> coalesce("a", "b")
a
> coalesce("", "b")
b
> coalesce(1,2)
1
```

https://developer.hashicorp.com/terraform/language/functions/coalesce

#### IAM.tf
``` IAM.tf
############################################################################
## AWSCloud9EnvironmentMemberポリシーのみをアタッチしたロール
############################################################################
# アカウント情報を取得
data "aws_caller_identity" "account" {}

# アカウントユーザーを対象とするassumeポリシー
data "aws_iam_policy_document" "assume" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = [data.aws_caller_identity.account.account_id]
    }
  }
}

# Cloud9共有用ロールを作成する
resource "aws_iam_role" "role" {
  name               = "cloud9_shared_role"
  assume_role_policy = data.aws_iam_policy_document.assume.json
}

# マネージドポリシー取得
data "aws_iam_policy" "policy" {
  arn = "arn:aws:iam::aws:policy/AWSCloud9EnvironmentMember"
}

# ポリシーをロールにアタッチ
resource "aws_iam_role_policy_attachment" "cloud9" {
  role       = aws_iam_role.role.name
  policy_arn = data.aws_iam_policy.policy.arn
}

############################################################################
## ロールにスイッチできる許可をあたえるIAMグループを作成
############################################################################
# IAMグループ
resource "aws_iam_group" "group" {
  name = "cloud9_shared_members"
  path = "/users/"
}

# cloud9ロールへのスイッチロールのみを許可するポリシードキュメント
data "aws_iam_policy_document" "sts" {
  statement {
    sid = "1"

    actions = [
      "sts:AssumeRole",
    ]

    resources = [
      aws_iam_role.role.arn,
    ]
  }
}

# グループにアタッチする独自ポリシー作成
resource "aws_iam_policy" "sts" {
  name   = "sts-cloud9-shared-policy"
  policy = data.aws_iam_policy_document.sts.json
}

# グループにポリシーをアタッチ
resource "aws_iam_group_policy_attachment" "sts" {
  group      = aws_iam_group.group.name
  policy_arn = aws_iam_policy.sts.arn
}
```

- 前述した、「Cloud9被共有者用ポリシーのみをアタッチしたロール」「左記ロールへのみスイッチできるユーザーグループ」を作成しています。

### 動作確認

![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/43a83234-74ed-9c1d-c589-28becc688b9c.png)

test-no-policy-userというユーザーをテスト用に作成します。

![cap2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/16dcd464-b666-f167-a99c-e30684c736ef.png)

動作確認のためグループに所属させず、初期ポリシーのみで作成を完了させます。

![cap3..PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/8a3f762f-26b8-1649-cd8c-b16963620b9f.png)

ログインしてみると、Read権限すらないことが確認できます。

![cap3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/640e4068-863e-e3e2-1b7b-052ebaeb151f.png)

Cloud9コンソールもエラーが多発します。

![cap4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/19d3e1d2-3d55-1f8d-372a-caba5f58847e.png)

確認のため、Cloud9の共有環境に招待してみます。

![cap6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/d6e6f6fc-4027-acd5-ecb9-4aaf2b558cff.png)

IDEにアクセスできますが、エラーメッセージが表示されます。

![cap7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/3815fe21-c2a8-a376-a356-2f02e4a804df.png)

用意したユーザーグループに「test-no-policy-user」を所属させます。

![cap8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/a4ec7019-f902-2884-214f-41cd5fafb7d1.png)

スイッチロールができました。Cloud9コンソールでのエラーも発生していません。

![cap9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ecfb977b-f97a-5653-c656-89fd1e5a4d45.png)

招待してみます。ロールにスイッチしたユーザーを招待する場合は、ユーザー名ではなく、```arn:aws:sts::${account_id}:assumed-role/${roll_id}/${user_id}```を招待します。
今回の場合、```arn:aws:sts::123456789012:assumed-role/cloud9_shared_role/test-no-policy-user```です。

参考：

https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/share-environment.html#share-environment-invite-user

![cap10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/677f03bd-71e0-030d-7729-36184dba255c.png)

無事エラーが発生しない状態で共有されました！

![cap11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ebb014c7-d1ac-1623-a0f0-cbfceee7921b.png)

また、その他リソースについては依然Readすらできない状態であることが確認できます。

## おわりに
Cloud9の環境共有機能、なかなか良いですよね。
ペア・モブプログラミングの土壌をクラウド上でサクッと構築できるのはありがたいです。

また、おまけとして、つい適当にしてしまっているIAMの最小権限化にも挑戦してみました。
FullAccessにしないことで必要なポリシーも自然と意識できるようになると思うので、今後もきちんと実施できればなと思います。