mattintosh note

どこかのエンジニアモドキの備忘録

孤独の AWS Cloud9 ハンズオン 〜Cloud9 は SSH 踏み台として使えるのか〜

先日、Amazon 本社で AWS スタートアップの説明を聞く機会があった。そこで「最近では踏み台に Cloud9 を使っている方も」という話を聞いたので「本当に踏み台として使えるのか」「Terraform との相性はどうなのか」というのを確認してみた。なお、Cloud9 には新規に EC2 インスタンスを作成するタイプと既存の EC2 インスタンスなどに Cloud9 IDE をインストールして Cloud9 environment として使う SSH タイプもあるが後者については触れない。

AWS Cloud9 とは

AWS Cloud9 はブラウザで使えるクラウド上の IDE。本来は開発目的で利用するものなんだろうが EC2 インスタンスを使っているので普通に踏み台として EC2 を構築するのとあまり変わらない。実際、既存の EC2 インスタンスに Cloud9 IDE をインストールしてそのまま「environment」として使うこともできる。

一般的な SSH 接続を中継する方法以外に Cloud9 IDE のターミナルからプライベートネットワークにあるインスタンスSSH 接続をするという選択肢がある。AWS マネジメントコンソールにアクセスでき、対象の「environment」でターミナルを使う権限があるユーザであれば SSH クライアントなしでプライベートネットワークに配置されているインスタンスにアクセスができる。ブラウザ上でファイル転送もできるので大抵のことは Cloud9 IDE 上でできるだろう。

他にも Cloud9 を使うメリットとしては以下のようなことが考えられる。

  • membership というもので IDE にアクセスできる(=ターミナルを利用できる)ユーザを管理することができる。
  • (Cloud9 IDE からしかアクセス出来ないと思われる)セキュリティグループを自動的に作成してくれる。名前は aws-cloud9-環境名-乱数 のような書式。
  • インアクティブな状態が続いた場合の自動シャットダウンを「30 分」「1 時間」「4 時間」「1 日」「1 週間」「なし」から設定することができる。踏み台用のインスタンスのコストを抑えることができる。

「お、これは結構使えるのでは?」と思ったが、試しているうちにデメリットも色々と出てきた。詳しくは後述するが、だいたい以下のような点。

  • Cloud9 用のインスタンスを作成したらサーバに接続するための秘密鍵を保存しておく必要がある。
  • 一般的な SSH 接続を中継する踏み台として利用する場合、セキュリティグループの編集が必要になったり、EIP を設定したりする必要がああるが自動化が難しい。

現時点での個人的な感想は「踏み台として『使える』か『使えない』かなら『使える』が、何か中途半端」といった感じ。

では孤独のハンズオンで色々と試していく。

Cloud9 environment の作成

Cloud9 では environment(以下、「Cloud9 environment」とする)と membership でアクセス制限を行っている。作成した Cloud9 environment の membership に IAM ユーザやロールを read-only または read-write のいずれかの権限で追加することによって複数のユーザで Cloud9 environment を共有することができるようになっている。

AWS マネジメントコンソールの Cloud9 メニューでは以下のように environment の種類が分かれている。

Your environment Owner が自身の ARN で登録されている Cloud9 environment が表示される。AWS マネジメントコンソールから作成した場合は必ずこちらに登録される。
Shared with you 対象の Cloud9 environment の membership に自身の ARN が read-only または read-write 権限で登録されている Cloud9 environment が表示される。
Account Environments AWS アカウントに登録されている Cloud9 environment が表示される。自身が Owner の Cloud9 environment 以外に、Owner でもなく membership でも登録されていない Cloud9 environment も表示される。Terraform や AWS CLI で Owner の ARN を指定しなかった場合はここにのみ表示される。

membership には read-onlyread-write 以外に owner があり、これは Cloud9 environment 作成時に自動的に作成される membership であり変更はできない。

AWS マネジメントコンソール、Terraform、AWS CLI で Cloud9 environment 作成時の挙動が異なるので以下にまとめておく。

AWS マネジメントコンソールから Cloud9 environment を作成する

「Name」は必須だが「Description」は任意。既に同名の Cloud9 environment が存在する場合は作成できない。(Owner が異なる場合は作成可能)

f:id:mattintosh4:20190714230009p:plain
AWS Cloud9

インスタンスタイプや配置する VPC、サブネットなどを選択する。インスタンスタイプは「Other instance type」を選択すれば好きなものを選ぶことができる。執筆時点ではここの「Amazon Linux」は「Amazon Linux 2」ではない。VPC やサブネットはデフォルトでは「デフォルト VPC」と「デフォルトサブネット」を使うようになっている。実際にはプロジェクト用に作成した VPC とサブネットに配置すると思うが、ここで気をつけなければならないのは Cloud9 environment を配置するサブネットはパブリックサブネットでなければならない。Cloud9 によって自動的に作成されるセキュリティグループを見ればわかるがインバウンドルールに Cloud9 用に予約されたネットワークからの 22 番接続を許可する設定が追加される。Cloud9 environment への接続へはグローバルから行われることになるため NAT ゲートウェイの有無に関わらずプライベートサブネットへ配置すると Cloud9 IDE にアクセスできなくなる。

f:id:mattintosh4:20190714230357p:plain
AWS Cloud9 - Create environment

設定内容を確認して[Create environment]ボタンをクリックする。

f:id:mattintosh4:20190714231315p:plain
AWS Cloud9 - Create environment

Cloud9 用のインスタンス作成が行われて接続画面に移行する。Cloud9 用の AMI から作成されているのでだいたい 60 秒もあれば使えるようになる。

f:id:mattintosh4:20190714231458p:plain
AWS Cloud9 - Cloud9 IDE

インスタンスの状態を確認してみる

f:id:mattintosh4:20190715011314p:plain

Cloud9 で作成される EC2 インスタンスとセキュリティグループは CloudFormation によって作成されているのでまずは CloudFormation のスタックを確認する。スタック名は aws-cloud9-{Cloud9環境名}-{Cloud9環境ID} となっている。Cloud9 environment の ID は Cloud9 IDE の URL か AWS CLIcloud9 list-environments で調べることができる。

aws cloudformation describe-stacks \
    --stack-name aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "StackName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "CreationTime": "2019-07-14T14:59:45.535Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Tags": [
                {
                    "Key": "aws:cloud9:environment",
                    "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                },
                {
                    "Key": "aws:cloud9:owner",
                    "Value": "XXXXXXXXXXXXXXXXXXXXX:john"
                }
            ],
            "EnableTerminationProtection": false
        }
    ]
}

スタックのリソースを確認する。

Terminal

aws cloudformation describe-stack-resources --stack-name aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

ここから EC2 インスタンスの ID とセキュリティグループの ID がわかる。

Result

{
    "StackResources": [
        {
            "StackName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "StackId": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "LogicalResourceId": "Instance",
            "PhysicalResourceId": "i-xxxxxxxxxxxxxxxxx",
            "ResourceType": "AWS::EC2::Instance",
            "Timestamp": "2019-07-14T15:00:31.775Z",
            "ResourceStatus": "CREATE_COMPLETE"
        },
        {
            "StackName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "StackId": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "LogicalResourceId": "InstanceSecurityGroup",
            "PhysicalResourceId": "sg-xxxxxxxxxxxxxxxxx",
            "ResourceType": "AWS::EC2::SecurityGroup",
            "Timestamp": "2019-07-14T14:59:55.347Z",
            "ResourceStatus": "CREATE_COMPLETE"
        }
    ]
}

セキュリティグループは aws-cloud9-{Cloud9環境名}-{Cloud9環境ID}-InstanceSecurityGroup-{乱数} という名前で作成されているので、GroupName かタグ値で CloudFormation のスタック名をフィルタリングすればすぐ出てくるだろう。

Terminal

aws ec2 describe-security-groups --filters 'Name=group-name,Values=aws-cloud9-foo-*'

セキュリティグループのインバウンドルールには Cloud9 で予約されていると思われるネットワークアドレスが二つ登録されている。

Result

{
    "SecurityGroups": [
        {
            "Description": "Security group for AWS Cloud9 environment aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "GroupName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-InstanceSecurityGroup-XXXXXXXXXXXXX",
            "IpPermissions": [
                {
                    "FromPort": 22,
                    "IpProtocol": "tcp",
                    "IpRanges": [
                        {
                            "CidrIp": "18.179.48.96/27"
                        },
                        {
                            "CidrIp": "18.179.48.128/27"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": [],
                    "ToPort": 22,
                    "UserIdGroupPairs": []
                }
            ],
            "OwnerId": "000000000000",
            "GroupId": "sg-xxxxxxxxxxxxxxxxx",
            "IpPermissionsEgress": [
                {
                    "IpProtocol": "-1",
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": [],
                    "UserIdGroupPairs": []
                }
            ],
            "Tags": [
                {
                    "Key": "aws:cloud9:owner",
                    "Value": "XXXXXXXXXXXXXXXXXXXXX:john"
                },
                {
                    "Key": "aws:cloudformation:stack-name",
                    "Value": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                },
                {
                    "Key": "aws:cloud9:environment",
                    "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                },
                {
                    "Key": "aws:cloudformation:logical-id",
                    "Value": "InstanceSecurityGroup"
                },
                {
                    "Key": "aws:cloudformation:stack-id",
                    "Value": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
                }
            ],
            "VpcId": "vpc-xxxxxxxx"
        }
    ]
}

EC2 インスタンスも同様。

Terminal

aws describe-instances --filters 'Name=tag-key,Values=Name' 'Name=tag-value,Values=aws-cloud9-foo-*'

{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 0,
                    "ImageId": "ami-0872d0e3184cfc976",
                    "InstanceId": "i-xxxxxxxxxxxxxxxxx",
                    "InstanceType": "t2.micro",
                    "LaunchTime": "2019-07-14T14:59:58.000Z",
                    "Monitoring": {
                        "State": "disabled"
                    },
                    "Placement": {
                        "AvailabilityZone": "ap-northeast-1a",
                        "GroupName": "",
                        "Tenancy": "default"
                    },
                    "PrivateDnsName": "ip-172-31-40-231.ap-northeast-1.compute.internal",
                    "PrivateIpAddress": "172.31.40.231",
                    "ProductCodes": [],
                    "PublicDnsName": "ec2-00-000-000-000.ap-northeast-1.compute.amazonaws.com",
                    "PublicIpAddress": "00.000.000.000",
                    "State": {
                        "Code": 16,
                        "Name": "running"
                    },
                    "StateTransitionReason": "",
                    "SubnetId": "subnet-xxxxxxxx",
                    "VpcId": "vpc-xxxxxxxx",
                    "Architecture": "x86_64",
                    "BlockDeviceMappings": [
                        {
                            "DeviceName": "/dev/xvda",
                            "Ebs": {
                                "AttachTime": "2019-07-14T14:59:59.000Z",
                                "DeleteOnTermination": true,
                                "Status": "attached",
                                "VolumeId": "vol-xxxxxxxxxxxxxxxxx"
                            }
                        }
                    ],
                    "ClientToken": "aws-c-Insta-XXXXXXXXXXXX",
                    "EbsOptimized": false,
                    "EnaSupport": true,
                    "Hypervisor": "xen",
                    "NetworkInterfaces": [
                        {
                            "Association": {
                                "IpOwnerId": "amazon",
                                "PublicDnsName": "ec2-00-000-000-000.ap-northeast-1.compute.amazonaws.com",
                                "PublicIp": "00.000.000.000"
                            },
                            "Attachment": {
                                "AttachTime": "2019-07-14T14:59:58.000Z",
                                "AttachmentId": "eni-attach-xxxxxxxxxxxxxxxxx",
                                "DeleteOnTermination": true,
                                "DeviceIndex": 0,
                                "Status": "attached"
                            },
                            "Description": "",
                            "Groups": [
                                {
                                    "GroupName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-InstanceSecurityGroup-XXXXXXXXXXXXX",
                                    "GroupId": "sg-xxxxxxxxxxxxxxxxx"
                                }
                            ],
                            "Ipv6Addresses": [],
                            "MacAddress": "xx:xx:xx:xx:xx:xx",
                            "NetworkInterfaceId": "eni-xxxxxxxxxxxxxxxxx",
                            "OwnerId": "000000000000",
                            "PrivateDnsName": "ip-172-31-40-231.ap-northeast-1.compute.internal",
                            "PrivateIpAddress": "172.31.40.231",
                            "PrivateIpAddresses": [
                                {
                                    "Association": {
                                        "IpOwnerId": "amazon",
                                        "PublicDnsName": "ec2-00-000-000-000.ap-northeast-1.compute.amazonaws.com",
                                        "PublicIp": "00.000.000.000"
                                    },
                                    "Primary": true,
                                    "PrivateDnsName": "ip-172-31-40-231.ap-northeast-1.compute.internal",
                                    "PrivateIpAddress": "172.31.40.231"
                                }
                            ],
                            "SourceDestCheck": true,
                            "Status": "in-use",
                            "SubnetId": "subnet-xxxxxxxx",
                            "VpcId": "vpc-xxxxxxxx"
                        }
                    ],
                    "RootDeviceName": "/dev/xvda",
                    "RootDeviceType": "ebs",
                    "SecurityGroups": [
                        {
                            "GroupName": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-InstanceSecurityGroup-XXXXXXXXXXXXX",
                            "GroupId": "sg-xxxxxxxxxxxxxxxxx"
                        }
                    ],
                    "SourceDestCheck": true,
                    "Tags": [
                        {
                            "Key": "aws:cloudformation:logical-id",
                            "Value": "Instance"
                        },
                        {
                            "Key": "Name",
                            "Value": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                        },
                        {
                            "Key": "aws:cloud9:environment",
                            "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                        },
                        {
                            "Key": "aws:cloudformation:stack-id",
                            "Value": "arn:aws:cloudformation:ap-northeast-1:000000000000:stack/aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
                        },
                        {
                            "Key": "aws:cloud9:owner",
                            "Value": "XXXXXXXXXXXXXXXXXXXXX:john"
                        },
                        {
                            "Key": "aws:cloudformation:stack-name",
                            "Value": "aws-cloud9-foo-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                        }
                    ],
                    "VirtualizationType": "hvm",
                    "CpuOptions": {
                        "CoreCount": 1,
                        "ThreadsPerCore": 1
                    }
                }
            ],
            "OwnerId": "000000000000",
            "RequesterId": "000000000000",
            "ReservationId": "r-xxxxxxxxxxxxxxxxx"
        }
    ]
}

いずれもタグに Cloud9 environment の ID が入っているのでフィルタリングはしやすい。

Cloud9 environment の menbership について学ぶ

AWS マネジメントコンソールから Cloud9 environment を作成した場合は現在の IAM ユーザやロールの ARN が Owner として設定されるようになっている。

Terminal

aws cloud9 describe-environments \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Result

{
    "environments": [
        {
            "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "name": "AWS Management Console",
            "description": "",
            "type": "ec2",
            "arn": "arn:aws:cloud9:ap-northeast-1:000000000000:environment:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "ownerArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
        }
    ]
}

上記の Cloud9 environment の membership がどうなっているか確認してみる。

Terminal

aws cloud9 describe-environment-memberships \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Result

{
    "memberships": [
        {
            "permissions": "owner",
            "userId": "XXXXXXXXXXXXXXXXXXXXX:john",
            "userArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john",
            "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "lastAccess": 1562846115.0
        }
    ]
}

memberships が配列になっていることからもわかるが membership には owner 以外に read-onlyread-write といった権限で複数のユーザを登録することができるようになっている。

Cloud9 environment と membership の関連性

Cloud9 environment の権限には ownerread-onlyread-write の 3 種類がある。AWS マネジメントコンソールから Cloud9 environment を作成した場合、owner には現在の IAM ユーザやロールが設定される。owner は当然のことながら変更や削除などすべての操作を行うことができる。

Cloud9 environment には membership というものがあり、この membership をカスタマイズすることで environment を他のユーザと共有できるようになっている。現状の AWS マネジメントコンソールには membership を操作する画面は無く、Cloud9 IDE 内の共有設定で行うようになっている。

f:id:mattintosh4:20190712195322p:plain
AWS Cloud9 IDE

Cloud9 environment を AWS マネジメントコンソール、Terraform、AWS CLI からownerArn を指定せずにそれぞれ作成すると以下のようになる。なお、AWS マネジメントコンソールでは ownerArn の指定方法が無いため Owner には自動的に現在のユーザに設定される。

{
    "environments": [
        {
            "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "name": "AWS Management Console",
            "description": "",
            "type": "ec2",
            "arn": "arn:aws:cloud9:ap-northeast-1:000000000000:environment:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "ownerArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
        },
        {
            "id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            "name": "Terraform",
            "description": "",
            "type": "ec2",
            "arn": "arn:aws:cloud9:ap-northeast-1:000000000000:environment:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            "ownerArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/1562929437597457834"
        },
        {
            "id": "cccccccccccccccccccccccccccccccc",
            "name": "AWS CLI",
            "description": "",
            "type": "ec2",
            "arn": "arn:aws:cloud9:ap-northeast-1:000000000000:environment:cccccccccccccccccccccccccccccccc",
            "ownerArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/botocore-session-1562929475"
        }
    ]
}

それぞれの ownerArn を見てみると Terraform ではナノ秒タイムスタンプ、AWS CLI では botocore-session- 接頭辞にタイムスタンプが自動的に設定されている。これらの違いを AWS マネジメントコンソールで確認してみる。

まずは「Your environments」を見てみると AWS マネジメントコンソールで作成した Cloud9 environment が表示されている。自分で作成したので Permissions は Owner となっている。作成したはずの「Terraform」や「AWS CLI」は無い。

f:id:mattintosh4:20190712202037p:plain
AWS Cloud9

「Shared with you」を飛ばして「Account environments」を見てみる。先程は無かった「Terraform」と「AWS CLI」も表示されている。

f:id:mattintosh4:20190712202423p:plain
AWS Cloud9

ではこの「Terraform」や「AWS CLI」が使えるのかというのを「Open IDE」を押して試してみるがアクセス権が無いと言われる。

f:id:mattintosh4:20190712202938p:plain
AWS Cloud9

IDE を開くことができないので「Terraform」と「AWS CLI」は AWS マネジメントコンソールから共有設定をすることができず、AWS CLI から設定することになる。しかし、AWS CLI のロールはオーナーではない membership への DeleteEnvironmentMembership 権限が無いので後述する「membership の削除」で問題が発生することがある。

フローにしてみると下記のようになる。

f:id:mattintosh4:20190713012300p:plain
AWS Cloud9

自身が Cloud9 environment の Owner であれば AWS CLI から追加した membership を Cloud9 IDE から削除することができるが、最も困るのが Terraform で owner_arn を未指定で作成してしまった場合。Owner が Terraform で作成した ARN になっているので自身を read-write で membership を追加したとしても Cloud9 IDE に共有設定が表示されないので membership 操作が一切できない。

membership の追加、更新、削除

Cloud9 environment の ID を調べ方

作成した Cloud9 environment の ID を控え忘れてしまった場合は AWS マネジメントコンソールで「Open IDE」や「View Details」、「Edit」を開けば URL に ID が含まれているのでそれをコピーしてもいい。

AWS CLI では list-environments サブコマンドで ID の一覧を表示することができるが、この結果を describe-environments に渡さないと名前がわからない。

Terminal

aws cloud9 list-environments

Result

{
    "environmentIds": [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
        "cccccccccccccccccccccccccccccccc"
    ]
}

一つずつ調べるのは面倒だが list-environments の結果を --query オプションで整形して describe-environments --environment-ids の引数に渡せば名前付きで一覧を取り出すことができる。

Terminal

aws cloud9 describe-environments \
    --environment-ids $(aws cloud9 list-environments --query "environmentIds" --output text) \
    --query "environments[].[id, name]" \
    --output text

Result

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa        AWS Management Console
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb        Terraform
cccccccccccccccccccccccccccccccc        AWS CLI

membership を追加する

membership の追加には create-environment-membership サブコマンドを使う。ここでは例として IAM ユーザである aliceread-only で追加する。

Terminal

aws cloud9 create-environment-membership \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
    --user-arn arn:aws:iam::000000000000:user/alice \
    --permissions read-only

Result

{
    "membership": {
        "permissions": "read-only",
        "userId": "YYYYYYYYYYYYYYYYYYYYY",
        "userArn": "arn:aws:iam::000000000000:user/alice",
        "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
}

describe-environment-memberships で確認すると aliceread-only で追加されていることがわかる。

Terminal

aws cloud9 describe-environment-memberships \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Result

{
    "memberships": [
        {
            "permissions": "read-only",
            "userId": "YYYYYYYYYYYYYYYYYYYYY",
            "userArn": "arn:aws:iam::000000000000:user/alice",
            "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        },
        {
            "permissions": "owner",
            "userId": "XXXXXXXXXXXXXXXXXXXXX:john",
            "userArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john",
            "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "lastAccess": 1562920538.0
        }
    ]
}

membership を更新する

membership の更新には update-environment-membership サブコマンドを使う。

ここでは上で追加した alice の権限を read-only から read-write に変更する。

Terminal

aws cloud9 update-environment-membership \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
    --user-arn arn:aws:iam::000000000000:user/alice \
    --permissions read-write

Result

{
    "membership": {
        "permissions": "read-write",
        "userId": "YYYYYYYYYYYYYYYYYYYYY",
        "userArn": "arn:aws:iam::000000000000:user/alice",
        "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
}

再び describe-environment-memberships で確認すると alice の権限が read-write に変更されていることがわかる。

Terminal

aws cloud9 describe-environment-memberships \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Result

{
    "memberships": [
        {
            "permissions": "read-write",
            "userId": "YYYYYYYYYYYYYYYYYYYYY",
            "userArn": "arn:aws:iam::000000000000:user/alice",
            "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        },
        {
            "permissions": "owner",
            "userId": "XXXXXXXXXXXXXXXXXXXXX:john",
            "userArn": "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john",
            "environmentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "lastAccess": 1562920538.0
        }
    ]
}

membership を削除する

membership の削除には delete-environment-membership サブコマンドを使う。

成功した場合は何も表示されない。

Terminal

aws cloud9 delete-environment-membership \
    --environment-id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
    --user-arn arn:aws:iam::000000000000:user/alice

AccessDeniedException の例

先に説明したように AWS マネジメントコンソールで Cloud9 environment を作成した場合、AWS CLI からの操作には DeleteEnvironmentMembership 権限が無いためエラーになる。

Result

An error occurred (AccessDeniedException) when calling the DeleteEnvironmentMembership operation:
User arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/botocore-session-1562923366
is not authorized to perform: cloud9:DeleteEnvironmentMembership on resource: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Cloud9 と Terraform

Terraform v0.12.1

Terraform で Cloud9 を扱う場合は aws_cloud9_environment_ec2 リソースを使用する。現状では Cloud9 に関連するリソースはこれしか無い。

aws_cloud9_environment_ec2 を使うときの注意点

Cloud9 envionment の membership の説明に書いたとおり、owner_arn の指定を忘れると membership の操作ができないばかりか Cloud9 IDE に入ることすらできないので(使い方によるが)指定しておいた方が無難。

aws_cloud9_environment_ec2 に変更を加えた場合、大抵は destroy される。automatic_stop_time_minutes は modify になりそうだが、これも destroy の対象なので自動シャットダウンの時間を変更したければ Cloud9 IDE から変更する必要がある。

modify になるもの

  • name を変更する
  • description を変更する

destroy になるもの

  • instance_type を変更する
  • automatic_stop_time_minutes を設定(never から 30 など)または変更(30 から 60 など)する
  • owner_arn を設定または変更する

Terraform による Cloud9 environment の作成

nameinstance_type だけ指定すれば Cloud9 environment を作成することができる。

aws_cloud9.tf

resource "aws_cloud9_environment_ec2" "foo" {
  // STEP 1
  name          = "Terraform"
  instance_type = "t2.micro"
}

Terminal

terraform apply --target aws_cloud9_environment_ec2.foo
terraform state show     aws_cloud9_environment_ec2.foo

owner_arn にはユーザ名としてタイムスタンプが設定される。

# aws_cloud9_environment_ec2.foo: 
resource "aws_cloud9_environment_ec2" "foo" {
    arn           = "arn:aws:cloud9:ap-northeast-1:000000000000:environment:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    id            = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    instance_type = "t2.micro"
    name          = "foo"
    owner_arn     = "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/1562844915108062971"
    type          = "ec2"
}

STEP 2: owner_arn の指定

先程の TF ファイルを変更して owner_arn を追記する。Terraform に限らずだが owner の変更は出来ないため同一名の Cloud9 environment を作成する場合は再構築する必要がある。

aws_cloud9.tf

resource "aws_cloud9_environment_ec2" "foo" {
  // STEP 1
  name          = "foo"
  instance_type = "t2.micro"
  // STEP 2
  owner_arn     = "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
}

Terminal

terraform apply --target aws_cloud9_environment_ec2.foo
terraform state show     aws_cloud9_environment_ec2.foo

terraform state show で確認すると owner_arn が指定したとおりに設定されているのがわかる。

# aws_cloud9_environment_ec2.foo: 
resource "aws_cloud9_environment_ec2" "foo" {
    arn           = "arn:aws:cloud9:ap-northeast-1:000000000000:environment:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    id            = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    instance_type = "t2.micro"
    name          = "foo"
    owner_arn     = "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
    type          = "ec2"
}

STEP 3: automatic_stop_time_minutes を指定する

Cloud9 ではインアクティブな状態が続いたときに自動的に EC2 インスタンスを停止させることができる機能が用意されている。aws_cloud9_environment_ec2 リソースでは automatic_stop_time_minutes という引数(数値型)で指定する。未指定の場合は AWS CLI 同様に never(停止しない)となる。destroy が発生するので「時間を変更したいけれど Cloud9 environment は壊したくない」という場合は IDE を開いて「AWS Cloud9 > Pereferences > PROJECT SETTINGS > EC2 Instance」で設定を変更する。なお、AWS マネジメントコンソールからは変更できない。

aws_cloud9.tf

resource "aws_cloud9_environment_ec2" "foo" {
  // STEP 1
  name                        = "foo"
  instance_type               = "t2.micro"
  // STEP 2
  owner_arn                   = "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
  // STEP 3
  automatic_stop_time_minutes = 30
}

Terminal

terraform apply --target aws_cloud9_environment_ec2.foo
terraform state show     aws_cloud9_environment_ec2.foo

指定した場合は下記のように属性と値が表示される。Q & A に書いておくが設定によっては IDE 上で確認できる値と一致しなくなる。指定しなかった場合は自動的に never となり automatic_stop_time_minutes 属性は出てこない。

Result

# aws_cloud9_environment_ec2.foo: 
resource "aws_cloud9_environment_ec2" "foo" {
    arn                         = "arn:aws:cloud9:ap-northeast-1:000000000000:environment:cccccccccccccccccccccccccccccccc"
    automatic_stop_time_minutes = 30
    id                          = "cccccccccccccccccccccccccccccccc"
    instance_type               = "t2.nano"
    name                        = "bar"
    owner_arn                   = "arn:aws:sts::000000000000:assumed-role/OrganizationAccountAccessRole/john"
    type                        = "ec2"
}

EIP の設定やセキュリティグループのカスタマイズの自動化はできるのか

Cloud9 envionment には EIP の設定が無いので自動シャットダウンが行われればパブリック IP アドレスは変わってしまうし、セキュリティグループは自動作成以外の選択肢が無いのでインバウンドルールを追加しないと本来の「踏み台」としては使えない。

しかし、Cloud9 で EC2 インスタンスを作成してもステータスには EC2 インスタンスの詳細情報は含まれていない。これは AWS CLI を使っても同じである。EC2 インスタンスやセキュリティグループは CloudFormation によって作成されているので Name タグには aws-cloud9-環境名-環境ID という書式で名前が設定されている。CloudFormation の aws:cloud9:environment タグにも Cloud9 environment の ID が入っているのでここからリソースを検索することもできるだろう。しかし、CloudFormation のスタックが Terraform の管理外で作成されているので変数呼び出しができない。

EC2 インスタンスの作成を通常通り行い、そちらで Cloud9 IDE のインストールと EIP やセキュリティグループを設定したあとに Cloud9 に SSH タイプで追加すれば…という方法も思いつくが残念ながらいまのところ Terraform には aws_cloud9_environment_ssh というリソースは存在しない。

従って、Cloud9 で EC2 インスタンスを新規に作成する場合は「踏み台」として使うための設定の自動化は簡単ではないと思われる。

Try & Result

Try: owner_arn を指定し忘れたのだが Cloud9 IDE を使いたい

AWS CLIcloud9 create-environment-membership で使用するユーザを membership に追加すればいい。ただし、Cloud9 IDE に membership の設定が表示されず GUI から membership の編集や削除ができないなどの制限があるので潔く作り直したほうがよい。

Try: automatic_stop_time_minutes に適当な値(たとえば 45 など)を設定するとどうなるか

terraform show では設定した通りの値が入っているが Cloud9 IDE で確認してみると切り上げられてプリセット値に設定されていた。(45 の場合であれば 1 時間)

Try: automatic_stop_time_minutes に明示的に never を指定するにはどうするのか

数値型なので automatic_stop_time_minutes = 0 と指定すれば never になる。

まとめ

一般的な「踏み台」として使わずに Cloud9 IDESSH ログイン用のスポットとして使う分にはいいと思うが、リモートサーバにログインするための秘密鍵を配置する必要があるなどいくつかの手間は残る。

個人的にはシェルのキーバインドを多用するので Ctrl + W(1単語前を消す)なんて押そうものならタブが落ちてしまうため Cloud9 IDE 上のターミナルは少々使いにくかったりする。

フロントの開発をやっているエンジニアさん相手には Proxy Jump の説明をするのも一苦労だったりするので AWS アカウントだけあれば使えるというのは便利なのかもしれない。しかし、プロジェクトで複数の人間が同じ Cloud9 IDE を使う場合はあまり向かないと思う。というのも、Cloud9 IDE を共有したとして現状ではそのログイン履歴を記録する方法が無いように思えるし、Linux システムのユーザ切り替えも行われない。これは例えば VNC などで画面共有をしているのとあまり変わらない。便利ではあるが、誰が SSH のコマンドを発行したのかわからないのは内部セキュリティ的にあまりよろしく無いように思える。

他のプロジェクトが Cloud9 をどのように SSH の踏み台として使っているのかは知らないがベストプラクティス的なものが教えてもらいたいものである。