AWS Cloud9 環境を作成したら裏で CloudFormation が動いていたので内容を確認してみた

Cloud9 環境の作成時に EC2 インスタンスを作成するための CloudFormation スタックが実行されます。環境タイプによりその内訳は微妙に異なります。

コンバンハ、千葉(幸)です。

AWS Cloud9 はクラウドベースの統合開発環境(IDE)です。Cloud9 環境を作成することで、ユーザーはブラウザ上の AWS Cloud9 IDE を使用して対話的に環境を使用できます。

Cloud9 環境のタイプとして以下3種類があり、それぞれのコンピューティングリソースも環境に接続します。

  • EC2 環境(直接接続)
  • EC2 環境(インバウンドなし、Systems Manager 経由)
  • SSH 環境(任意の既存のサーバ)

EC2 環境を選択した場合、Cloud9 環境の作成にあわせて EC2 インスタンスも作成されます。

その作成が CloudFormation で行われていたため、内容を確認してみました。

きっかけは AWS Cloud9 のサービスロール

CloudFormation が動いていることに気づいたきっかけは、 AWS Cloud9 で使用されるサービスロールAWSServiceRoleForAWSCloud9が持つ権限を確認したことでした。

アタッチされているポリシーAWSCloud9ServiceRolePolicy内訳はこのようになっています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:RunInstances",
                "ec2:CreateSecurityGroup",
                "ec2:DescribeVpcs",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceStatus",
                "cloudformation:CreateStack",
                "cloudformation:DescribeStacks",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStackResources"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:TerminateInstances",
                "ec2:DeleteSecurityGroup",
                "ec2:AuthorizeSecurityGroupIngress"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:DeleteStack"
            ],
            "Resource": "arn:aws:cloudformation:*:*:stack/aws-cloud9-*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:instance/*",
                "arn:aws:ec2:*:*:security-group/*"
            ],
            "Condition": {
                "StringLike": {
                    "aws:RequestTag/Name": "aws-cloud9-*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/aws:cloudformation:stack-name": "aws-cloud9-*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:ListInstanceProfiles",
                "iam:GetInstanceProfile"
            ],
            "Resource": [
                "arn:aws:iam::*:instance-profile/cloud9/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": [
                "arn:aws:iam::*:role/service-role/AWSCloud9SSMAccessRole"
            ],
            "Condition": {
                "StringLike": {
                    "iam:PassedToService": "ec2.amazonaws.com"
                }
            }
        }
    ]
}

EC2 インスタンスの操作の他に、CloudFormation に関する権限が定義されていますね。

Cloud9 用 Cfn テンプレートの内容を確認

実際に CloudFormation のコンソールを確認すると、aws-cloud9-というプレフィックスを持つスタックが作成されています。

Cloud 9 環境を削除すると CloudFormation スタックも削除されます。検証のために作成や削除を繰り返したので、わたしの環境では多くのスタックが表示されます。

スタックからテンプレートが確認できるため、その内容をチェックしていきます。

ちなみに、Cloud9 環境作成時には大まかに以下のパラメータを指定できます。

  • 環境名
  • 環境タイプ
  • インスタンスタイプ
  • プラットフォーム
  • 配置 VPC、サブネット

EC2 環境の場合

テンプレートは以下の通りです。

{
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-0b397970e53f7d4f0",
        "InstanceType": "t2.micro",
        "UserData": ...略...,
        "Tags": [
          {
            "Key": "Name",
            "Value": "aws-cloud9-環境名-環境ID"
          }
        ],
        "NetworkInterfaces": [
          {
            "AssociatePublicIpAddress": true,
            "DeviceIndex": 0,
            "SubnetId": "subnet-0caa45223899b4b73",
            "GroupSet": [{"Ref": "InstanceSecurityGroup"}]
          }
        ]
      }
    },
    "InstanceSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Security group for AWS Cloud9 environment aws-cloud9-環境名-環境ID",
        "VpcId": "vpc-0e4acafc38414468c",
        "SecurityGroupIngress": [
          {
            "FromPort": 22,
            "ToPort": 22,
            "IpProtocol": "tcp",
            "CidrIp": "18.179.48.128/27"
          },
          {
            "FromPort": 22,
            "ToPort": 22,
            "IpProtocol": "tcp",
            "CidrIp": "18.179.48.96/27"
          }
        ]
        ,"Tags": [
          {
            "Key": "Name",
            "Value": "aws-cloud9-環境名-環境ID"
          }
        ]
      }
    }
  }
}

インスタンスタイプや配置 VPC、サブネットは環境作成時に指定したものが直接埋め込まれています。ユーザーデータは後で取り上げるとして、その他いくつかピックアップします。

パブリック IP アドレスの割り当て

AssociatePublicIpAddressが true となっています。よって、配置するサブネットの設定に依らず、ここで作成される EC2 インスタンスにはパブリック IP アドレスが付与されます。

Cloud9 用 AMI

今回の例ではプラットフォームとして Amazon Linux2 を指定しています。使用された AMI を確認してみます。

% aws ec2 describe-images --image-ids ami-0b397970e53f7d4f0 --output table
------------------------------------------------------------------------
|                            DescribeImages                            |
+----------------------------------------------------------------------+
||                               Images                               ||
|+---------------------+----------------------------------------------+|
||  Architecture       |  x86_64                                      ||
||  CreationDate       |  2021-08-03T05:14:16.000Z                    ||
||  Description        |  Cloud9 Cloud9AmazonLinux2 AMI               ||
||  EnaSupport         |  True                                        ||
||  Hypervisor         |  xen                                         ||
||  ImageId            |  ami-0b397970e53f7d4f0                       ||
||  ImageLocation      |  amazon/Cloud9AmazonLinux2-2021-08-03T04-25  ||
||  ImageOwnerAlias    |  amazon                                      ||
||  ImageType          |  machine                                     ||
||  Name               |  Cloud9AmazonLinux2-2021-08-03T04-25         ||
||  OwnerId            |  465558535106                                ||
||  PlatformDetails    |  Linux/UNIX                                  ||
||  Public             |  True                                        ||
||  RootDeviceName     |  /dev/xvda                                   ||
||  RootDeviceType     |  ebs                                         ||
||  SriovNetSupport    |  simple                                      ||
||  State              |  available                                   ||
||  UsageOperation     |  RunInstances                                ||
||  VirtualizationType |  hvm                                         ||
|+---------------------+----------------------------------------------+|
|||                        BlockDeviceMappings                       |||
||+---------------------------------+--------------------------------+||
|||  DeviceName                     |  /dev/xvda                     |||
||+---------------------------------+--------------------------------+||
||||                               Ebs                              ||||
|||+-----------------------------+----------------------------------+|||
||||  DeleteOnTermination        |  True                            ||||
||||  Encrypted                  |  False                           ||||
||||  SnapshotId                 |  snap-073c7086836ec34ce          ||||
||||  VolumeSize                 |  10                              ||||
||||  VolumeType                 |  gp2                             ||||
|||+-----------------------------+----------------------------------+|||

構築当時で最新と思われる AMI が使用されています。AMI 名や Description から、Cloud9 用の AMI が用意されていることが読み取れます。

SecuriryGroup

特定の CIDR からの SSH でのインバウンドが許可された SecuriryGroup が作成されています。

以下からダウンロードできるip-ranges.jsonから確認すると、サービスコードCLOUD9に属する IP プレフィックスであることがわかります。

% jq -r '.prefixes[] | select(.region=="ap-northeast-1" and .service=="CLOUD9") | .ip_prefix' < ip-ranges.json
18.179.48.96/27
18.179.48.128/27

Ingress なしの EC2 環境の場合

テンプレートは以下の通りです。

{
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-0b397970e53f7d4f0",
        "InstanceType": "t3.small",
        "IamInstanceProfile": "AWSCloud9SSMInstanceProfile",
        "UserData": ...略...,
        "Tags": [
          {
            "Key": "Name",
            "Value": "aws-cloud9-環境名-環境ID"
          }
        ],
        "NetworkInterfaces": [
          {
            "DeviceIndex": 0,
            "SubnetId": "subnet-09b107b0d68026bb8",
            "GroupSet": [{"Ref": "InstanceSecurityGroup"}]
          }
        ]
      }
    },
    "InstanceSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Security group for AWS Cloud9 environment aws-cloud9-環境名-環境ID",
        "VpcId": "vpc-0e4acafc38414468c"
        ,"Tags": [
          {
            "Key": "Name",
            "Value": "aws-cloud9-環境名-環境ID"
          }
        ]
      }
    }
  }
}

EC2 環境との差異を見ていきます。

インスタンスプロファイル

AWSCloud9SSMInstanceProfileというインスタンスプロファイルが指定されています。

該当のインスタンスプロファイルと関連づけられている IAM ロールはAWSCloud9SSMAccessRoleであり、以下の IAM ポリシーがアタッチされています。

arn:aws:iam::aws:policy/AWSCloud9SSMInstanceProfile

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel",
                "ssm:UpdateInstanceInformation"
            ],
            "Resource": "*"
        }
    ]
}

Systems Manager による接続に必要な権限のみが与えられています。

パブリック IP アドレスの割り当て

"AssociatePublicIpAddress": trueの定義がないため、インスタンスとしてパブリック IP アドレスの割り当ては有効になりません。

Ingress なしの EC2 環境はパブリックサブネットにもプライベートサブネットにも配置ができますが、パブリックサブネットに配置する場合、サブネット側でパブリック IP アドレスの割り当てが有効になっている必要があります。(もしくは後から EIP をアタッチする。)

SecuriryGroup

インバウンドが不要のため、以下のルールを持つ SecuriryGroup が作成されます。

  • インバウンドルール:なし
  • アウトバウンドルール:0.0.0.0/0 向けにすべてのトラフィックを許可

Cloud9 用ユーザーデータの内容を確認

先ほどテンプレートの記載を省略したユーザーデータは、以下のような値を持ちます。

一部を除き、どのテンプレートでも内容は同一です。

IyEvYmluL2Jhc2gKClVOSVhfVVNFUj0iZWMyLXVzZXIiClVOSVhfVVNFUl9IT01FPSIvaG9tZS9lYzItdXNlciIKRU5WSVJPTk1FTlRfUEFUSD0iL2hvbWUvZWMyLXVzZXIvZW52aXJvbm1lbnQiClVOSVhfR1JPVVA9JChpZCAtZyAtbiAiJFVOSVhfVVNFUiIpCgojIEFwcGx5IHNlY3VyaXR5IHBhdGNoZXMKT1BFUkFUSU5HX1NZU1RFTT0kKGF3ayAtRj0gJyQxPT0iSUQiIHsgcHJpbnQgJDIgO30nIC9ldGMvb3MtcmVsZWFzZSB8IHNlZCAtZSAncy9eIi8vJyAtZSAncy8iJC8vJykKaWYgWyAiJE9QRVJBVElOR19TWVNURU0iID09ICJhbXpuIiBdOyB0aGVuCiAgICB5dW0gLXEgLXkgdXBkYXRlIC0tc2VjdXJpdHkgPiAvdG1wL2luaXQteXVtLXVwZGF0ZS1zZWN1cml0eSAyPiYxICYKZWxpZiBbICIkT1BFUkFUSU5HX1NZU1RFTSIgPT0gInVidW50dSIgXTsgdGhlbgogICAgdW5hdHRlbmRlZC11cGdyYWRlICYKZmkKCiMgYWRkIFNTSCBrZXkKaW5zdGFsbCAtZyAiJFVOSVhfR1JPVVAiIC1vICIkVU5JWF9VU0VSIiAtbSA3NTUgLWQgIiRVTklYX1VTRVJfSE9NRSIvLnNzaApjYXQgPDwnRU9GJyA+PiAiJFVOSVhfVVNFUl9IT01FIi8uc3NoL2F1dGhvcml6ZWRfa2V5cwojIEltcG9ydGFudAojIC0tLS0tLS0tLQojIFRoZSBmb2xsb3dpbmcgcHVibGljIGtleSBpcyByZXF1aXJlZCBieSBDbG91ZDkgSURFCiMgUmVtb3ZpbmcgdGhpcyBrZXkgd2lsbCBtYWtlIHRoaXMgRUMyIGluc3RhbmNlIGluYWNjZXNzaWJsZSBieSB0aGUgSURFCiMKY2VydC1hdXRob3JpdHkgc3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFDQVFERXFlcWhMR2xNK2xSOHcwRnBrYlR3ZnJWKzA4VXV4STFiTzZjSzJQMFRMUFp3L3ZRTlFmRU1yejJRMVJueFczMHdpQzFLL254allLRmQwNVpKZzZ0T1Jhc1RQUTdUSml4ZitQM2JKdVhaRmd3dmNsajRxU3VhckpVOFVCaW1UMnVzMjZobHA0RmtydTVjd0ZRUUtsM3JJRk9LVkdhT0RBS05FMUp0bHlLN3F2QkppWjRLQnlKd2FXeG5uU0JoblIxK0RTL1M3U25DYUY3ZGhtSE5SQjM4SFJlVDY4TEFQaElOMXh2bzcvZnNCaHVBa05DZ2VhdXl0RTRHSGRlVDcwT1ZKVjFQSWI3bjM0aG5nL3BwZS9PR2FxOHZOZE9wY3Nac2lJeDhTNTRiSGlNQUQ3Z0pHSld6NWRWMEpyc2VzZG14WjZmT1gxTVZza0YrN3Y0ckw0cGt6c2svYzlSbStyQ2pXUW9tUHBzQVR2ZnBzdkZxVFQ4OUppZEIweDlaaXF1ck9lcHN6Nk9TVXFmUmxrNytFSXBRNGZiVFoxZ2JqTlRlcDlCUXQ3Z3hLdmVVL3BxNmR6ZGprMEVOS1FpeVhtWDgzTFRHdFg0OG04a2tITm9mTnkyUWtoRFFiQmRpS2JkdVcvVCt5NG53M3VDYzU0d01qTkxqd1dSY3c0U0VwVTAxNnBnZ3lWTnliWUNmc2VCRFJuNFRnT2pRWWZEZ3ZjQWVwd01Ma3prR1NXSEExWTIzSGV5bFVIWGNZb1MrY1dPaDZEZjhMbFF3ZWNZemlPa0liYnFTZXdLMERVRlRhZHY3RlpLU2hTYVlrbWtpTE1Zb1orSTQrQ0F6Y3NvQUxRbXV5cS8xVTJqYlMvUTRHWGRWN2lBdTE3TE9hTWVUT052SkRtNWtVUnNVWFE9PSBjNWIyMWEyZjM1MWQ0YjdkOGExMDRjZWViNGJiNTI0MkBjbG91ZDkuYW1hem9uLmNvbQoKCiMKIyBBZGQgYW55IGFkZGl0aW9uYWwga2V5cyBiZWxvdyB0aGlzIGxpbmUKIwoKRU9GCgojIGFsbG93IGF1dG9tYXRpYyBzaHV0ZG93bgplY2hvICIkVU5JWF9VU0VSICAgIEFMTD0oQUxMKSBOT1BBU1NXRDogL3NiaW4vcG93ZXJvZmYsIC9zYmluL3JlYm9vdCwgL3NiaW4vc2h1dGRvd24iID4+IC9ldGMvc3Vkb2VycwoKbG4gLXMgL29wdC9jOSAiJFVOSVhfVVNFUl9IT01FIi8uYzkKY2hvd24gLVIgIiRVTklYX1VTRVIiOiIkVU5JWF9HUk9VUCIgIiRVTklYX1VTRVJfSE9NRSIvLmM5IC9vcHQvYzkKaW5zdGFsbCAtZyAiJFVOSVhfR1JPVVAiIC1vICIkVU5JWF9VU0VSIiAtbSA3NTUgLWQgIiRFTlZJUk9OTUVOVF9QQVRIIgoKaWYgWyAiJEVOVklST05NRU5UX1BBVEgiID09ICIvaG9tZS9lYzItdXNlci9lbnZpcm9ubWVudCIgXSAmJiBncmVwICJhbGlhcyBweXRob249cHl0aG9uMjciICIkVU5JWF9VU0VSX0hPTUUiLy5iYXNocmM7IHRoZW4KCiAgICBjYXQgPDwnRU9GJyA+ICIkVU5JWF9VU0VSX0hPTUUiLy5iYXNocmMKIyAuYmFzaHJjCgpleHBvcnQgUEFUSD0kUEFUSDokSE9NRS8ubG9jYWwvYmluOiRIT01FL2JpbgoKIyBsb2FkIG52bQpleHBvcnQgTlZNX0RJUj0iJEhPTUUvLm52bSIKWyAiJEJBU0hfVkVSU0lPTiIgXSAmJiBucG0oKSB7CiAgICAjIGhhY2s6IGF2b2lkIHNsb3cgbnBtIHNhbml0eSBjaGVjayBpbiBudm0KICAgIGlmIFsgIiQqIiA9PSAiY29uZmlnIGdldCBwcmVmaXgiIF07IHRoZW4gd2hpY2ggbm9kZSB8IHNlZCAicy9iaW5cL25vZGUvLyI7CiAgICBlbHNlICQod2hpY2ggbnBtKSAiJEAiOyBmaQp9CiMgWyAtcyAiJE5WTV9ESVIvbnZtLnNoIiBdICYmIC4gIiROVk1fRElSL252bS5zaCIgICMgVGhpcyBsb2FkcyBudm0KcnZtX3NpbGVuY2VfcGF0aF9taXNtYXRjaF9jaGVja19mbGFnPTEgIyBwcmV2ZW50IHJ2bSBjb21wbGFpbnRzIHRoYXQgbnZtIGlzIGZpcnN0IGluIFBBVEgKdW5zZXQgbnBtICMgZW5kIGhhY2sKCgojIFVzZXIgc3BlY2lmaWMgYWxpYXNlcyBhbmQgZnVuY3Rpb25zCmFsaWFzIHB5dGhvbj1weXRob24yNwoKIyBtb2RpZmljYXRpb25zIG5lZWRlZCBvbmx5IGluIGludGVyYWN0aXZlIG1vZGUKaWYgWyAiJFBTMSIgIT0gIiIgXTsgdGhlbgogICAgIyBTZXQgZGVmYXVsdCBlZGl0b3IgZm9yIGdpdAogICAgZ2l0IGNvbmZpZyAtLWdsb2JhbCBjb3JlLmVkaXRvciBuYW5vCgogICAgIyBUdXJuIG9uIGNoZWNrd2luc2l6ZQogICAgc2hvcHQgLXMgY2hlY2t3aW5zaXplCgogICAgIyBrZWVwIG1vcmUgaGlzdG9yeQogICAgc2hvcHQgLXMgaGlzdGFwcGVuZAogICAgZXhwb3J0IEhJU1RTSVpFPTEwMDAwMAogICAgZXhwb3J0IEhJU1RGSUxFU0laRT0xMDAwMDAKICAgIGV4cG9ydCBQUk9NUFRfQ09NTUFORD0iaGlzdG9yeSAtYTsiCgogICAgIyBTb3VyY2UgZm9yIEdpdCBQUzEgZnVuY3Rpb24KICAgIGlmICEgdHlwZSAtdCBfX2dpdF9wczEgJiYgWyAtZSAiL3Vzci9zaGFyZS9naXQtY29yZS9jb250cmliL2NvbXBsZXRpb24vZ2l0LXByb21wdC5zaCIgXTsgdGhlbgogICAgICAgIC4gL3Vzci9zaGFyZS9naXQtY29yZS9jb250cmliL2NvbXBsZXRpb24vZ2l0LXByb21wdC5zaAogICAgZmkKCiAgICAjIENsb3VkOSBkZWZhdWx0IHByb21wdAogICAgX2Nsb3VkOV9wcm9tcHRfdXNlcigpIHsKICAgICAgICBpZiBbICIkQzlfVVNFUiIgPSByb290IF07IHRoZW4KICAgICAgICAgICAgZWNobyAiJFVTRVIiCiAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICIkQzlfVVNFUiIKICAgICAgICBmaQogICAgfQoKICAgIFBTMT0nXFtcMDMzWzAxOzMybVxdJChfY2xvdWQ5X3Byb21wdF91c2VyKVxbXDAzM1swMG1cXTpcW1wwMzNbMDE7MzRtXF1cd1xbXDAzM1swMG1cXSQoX19naXRfcHMxICIgKCVzKSIgMj4vZGV2L251bGwpICQgJwpmaQoKRU9GCgogICAgY2hvd24gIiRVTklYX1VTRVIiOiIkVU5JWF9HUk9VUCIgIiRVTklYX1VTRVJfSE9NRSIvLmJhc2hyYwpmaQoKaWYgWyAiJEVOVklST05NRU5UX1BBVEgiID09ICIvaG9tZS9lYzItdXNlci9lbnZpcm9ubWVudCIgXSAmJiBbICEgLWYgIiRFTlZJUk9OTUVOVF9QQVRIIi9SRUFETUUubWQgXTsgdGhlbgogICAgY2F0IDw8J0VPRicgPj4gIiRFTlZJUk9OTUVOVF9QQVRIIi9SRUFETUUubWQKICAgICAgICAgX19fICAgICAgICBfX19fX18gICAgIF9fX18gXyAgICAgICAgICAgICAgICAgXyAgX19fCiAgICAgICAgLyBcIFwgICAgICAvIC8gX19ffCAgIC8gX19ffCB8IF9fXyAgXyAgIF8gIF9ffCB8LyBfIFwKICAgICAgIC8gXyBcIFwgL1wgLyAvXF9fXyBcICB8IHwgICB8IHwvIF8gXHwgfCB8IHwvIF9gIHwgKF8pIHwKICAgICAgLyBfX18gXCBWICBWIC8gIF9fXykgfCB8IHxfX198IHwgKF8pIHwgfF98IHwgKF98IHxcX18sIHwKICAgICAvXy8gICBcX1xfL1xfLyAgfF9fX18vICAgXF9fX198X3xcX19fLyBcX18sX3xcX18sX3wgIC9fLwogLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCgpIaSB0aGVyZSEgV2VsY29tZSB0byBBV1MgQ2xvdWQ5IQoKVG8gZ2V0IHN0YXJ0ZWQsIGNyZWF0ZSBzb21lIGZpbGVzLCBwbGF5IHdpdGggdGhlIHRlcm1pbmFsLApvciB2aXNpdCBodHRwczovL2RvY3MuYXdzLmFtYXpvbi5jb20vY29uc29sZS9jbG91ZDkvIGZvciBvdXIgZG9jdW1lbnRhdGlvbi4KCkhhcHB5IGNvZGluZyEKCkVPRgoKICAgIGNob3duICIkVU5JWF9VU0VSIjoiJFVOSVhfR1JPVVAiICIkVU5JWF9VU0VSX0hPTUUiL2Vudmlyb25tZW50L1JFQURNRS5tZApmaQoKIyBGaXggZm9yIHBlcm1pc3Npb24gZXJyb3Igd2hlbiB0cnlpbmcgdG8gY2FsbCBgZ2VtIGluc3RhbGxgCmNob3duICIkVU5JWF9VU0VSIiAtUiAvdXNyL2xvY2FsL3J2bS9nZW1zCgojVGhpcyBzY3JpcHQgaXMgYXBwZW5kZWQgdG8gYW5vdGhlciBiYXNoIHNjcmlwdCwgc28gaXQgZG9lcyBub3QgbmVlZCBhIGJhc2ggc2NyaXB0IGZpbGUgaGVhZGVyLgoKVU5JWF9VU0VSX0hPTUU9Ii9ob21lL2VjMi11c2VyIgoKQzlfRElSPSRVTklYX1VTRVJfSE9NRS8uYzkKQ09ORklHX0ZJTEVfUEFUSD0iJEM5X0RJUiIvYXV0b3NodXRkb3duLWNvbmZpZ3VyYXRpb24KVkZTX0NIRUNLX0ZJTEVfUEFUSD0iJEM5X0RJUiIvc3RvcC1pZi1pbmFjdGl2ZS5zaAoKZWNobyAiU0hVVERPV05fVElNRU9VVD0zMCIgPiAiJENPTkZJR19GSUxFX1BBVEgiCmNobW9kIGErdyAiJENPTkZJR19GSUxFX1BBVEgiCgplY2hvIC1lICcjIS9iaW4vYmFzaApzZXQgLWV1byBwaXBlZmFpbApDT05GSUc9JChjYXQgJyRDT05GSUdfRklMRV9QQVRIJykKU0hVVERPV05fVElNRU9VVD0ke0NPTkZJRyMqPX0KaWYgISBbWyAkU0hVVERPV05fVElNRU9VVCA9fiBeWzAtOV0qJCBdXTsgdGhlbgogICAgZWNobyAic2h1dGRvd24gdGltZW91dCBpcyBpbnZhbGlkIgogICAgZXhpdCAxCmZpCmlzX3NodXR0aW5nX2Rvd24oKSB7CiAgICBpc19zaHV0dGluZ19kb3duX3VidW50dSAmPiAvZGV2L251bGwgfHwgaXNfc2h1dHRpbmdfZG93bl9hbDEgJj4gL2Rldi9udWxsIHx8IGlzX3NodXR0aW5nX2Rvd25fYWwyICY+IC9kZXYvbnVsbAp9CmlzX3NodXR0aW5nX2Rvd25fdWJ1bnR1KCkgewogICAgbG9jYWwgVElNRU9VVAogICAgVElNRU9VVD0kKGJ1c2N0bCBnZXQtcHJvcGVydHkgb3JnLmZyZWVkZXNrdG9wLmxvZ2luMSAvb3JnL2ZyZWVkZXNrdG9wL2xvZ2luMSBvcmcuZnJlZWRlc2t0b3AubG9naW4xLk1hbmFnZXIgU2NoZWR1bGVkU2h1dGRvd24pCiAgICBpZiBbICIkPyIgLW5lICIwIiBdOyB0aGVuCiAgICAgICAgcmV0dXJuIDEKICAgIGZpCiAgICBpZiBbICIkKGVjaG8gJFRJTUVPVVQgfCBhd2sgIntwcmludCBcJDN9IikiID09ICIwIiBdOyB0aGVuCiAgICAgICAgcmV0dXJuIDEKICAgIGVsc2UKICAgICAgICByZXR1cm4gMAogICAgZmkKfQppc19zaHV0dGluZ19kb3duX2FsMSgpIHsKICAgIHBncmVwIHNodXRkb3duCn0KaXNfc2h1dHRpbmdfZG93bl9hbDIoKSB7CiAgICBsb2NhbCBGSUxFCiAgICBGSUxFPS9ydW4vc3lzdGVtZC9zaHV0ZG93bi9zY2hlZHVsZWQKICAgIGlmIFtbIC1mICIkRklMRSIgXV07IHRoZW4KICAgICAgICByZXR1cm4gMAogICAgZWxzZQogICAgICAgIHJldHVybiAxCiAgICBmaQp9CmlzX3Zmc19jb25uZWN0ZWQoKSB7CiAgICBwZ3JlcCAtZiB2ZnMtd29ya2VyID4vZGV2L251bGwKfQoKaWYgaXNfc2h1dHRpbmdfZG93bjsgdGhlbgogICAgaWYgW1sgISAkU0hVVERPV05fVElNRU9VVCA9fiBeWzAtOV0rJCBdXSB8fCBpc192ZnNfY29ubmVjdGVkOyB0aGVuCiAgICAgICAgc3VkbyBzaHV0ZG93biAtYwogICAgZmkKZWxzZQogICAgaWYgW1sgJFNIVVRET1dOX1RJTUVPVVQgPX4gXlswLTldKyQgXV0gJiYgISBpc192ZnNfY29ubmVjdGVkOyB0aGVuCiAgICAgICAgc3VkbyBzaHV0ZG93biAtaCAkU0hVVERPV05fVElNRU9VVAogICAgZmkKZmknID4gIiRWRlNfQ0hFQ0tfRklMRV9QQVRIIgoKY2htb2QgK3ggIiRWRlNfQ0hFQ0tfRklMRV9QQVRIIgoKZWNobyAiKiAqICogKiAqIHJvb3QgJFZGU19DSEVDS19GSUxFX1BBVEgiID4gL2V0Yy9jcm9uLmQvYzktYXV0b21hdGljLXNodXRkb3duCg==

ユーザーデータは base64 エンコードされた状態のため、デコードしてみます。

ハイライトしている箇所は Cloud9 IDE からの接続に使用される公開鍵で、この値は環境ごとに異なるものが払い出されます。

# 環境変数 USERDATA に値を格納済みとして
% echo $USERDATA | base64 -d
#!/bin/bash

UNIX_USER="ec2-user"
UNIX_USER_HOME="/home/ec2-user"
ENVIRONMENT_PATH="/home/ec2-user/environment"
UNIX_GROUP=$(id -g -n "$UNIX_USER")

# Apply security patches
OPERATING_SYSTEM=$(awk -F= '$1=="ID" { print $2 ;}' /etc/os-release | sed -e 's/^"//' -e 's/"$//')
if [ "$OPERATING_SYSTEM" == "amzn" ]; then
    yum -q -y update --security > /tmp/init-yum-update-security 2>&1 &
elif [ "$OPERATING_SYSTEM" == "ubuntu" ]; then
    unattended-upgrade &
fi

# add SSH key
install -g "$UNIX_GROUP" -o "$UNIX_USER" -m 755 -d "$UNIX_USER_HOME"/.ssh
cat <<'EOF' >> "$UNIX_USER_HOME"/.ssh/authorized_keys
# Important
# ---------
# The following public key is required by Cloud9 IDE
# Removing this key will make this EC2 instance inaccessible by the IDE
#
cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDEqeqhLGlM+lR8w0FpkbTwfrV+08UuxI1bO6cK2P0TLPZw/vQNQfEMrz2Q1RnxW30wiC1K/nxjYKFd05ZJg6tORasTPQ7TJixf+P3bJuXZFgwvclj4qSuarJU8UBimT2us26hlp4Fkru5cwFQQKl3rIFOKVGaODAKNE1JtlyK7qvBJiZ4KByJwaWxnnSBhnR1+DS/S7SnCaF7dhmHNRB38HReT68LAPhIN1xvo7/fsBhuAkNCgeauytE4GHdeT70OVJV1PIb7n34hng/ppe/OGaq8vNdOpcsZsiIx8S54bHiMAD7gJGJWz5dV0JrsesdmxZ6fOX1MVskF+7v4rL4pkzsk/c9Rm+rCjWQomPpsATvfpsvFqTT89JidB0x9ZiqurOepsz6OSUqfRlk7+EIpQ4fbTZ1gbjNTep9BQt7gxKveU/pq6dzdjk0ENKQiyXmX83LTGtX48m8kkHNofNy2QkhDQbBdiKbduW/T+y4nw3uCc54wMjNLjwWRcw4SEpU016pggyVNybYCfseBDRn4TgOjQYfDgvcAepwMLkzkGSWHA1Y23HeylUHXcYoS+cWOh6Df8LlQwecYziOkIbbqSewK0DUFTadv7FZKShSaYkmkiLMYoZ+I4+CAzcsoALQmuyq/1U2jbS/Q4GXdV7iAu17LOaMeTONvJDm5kURsUXQ== c5b21a2f351d4b7d8a104ceeb4bb5242@cloud9.amazon.com


#
# Add any additional keys below this line
#

EOF

# allow automatic shutdown
echo "$UNIX_USER    ALL=(ALL) NOPASSWD: /sbin/poweroff, /sbin/reboot, /sbin/shutdown" >> /etc/sudoers

ln -s /opt/c9 "$UNIX_USER_HOME"/.c9
chown -R "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/.c9 /opt/c9
install -g "$UNIX_GROUP" -o "$UNIX_USER" -m 755 -d "$ENVIRONMENT_PATH"

if [ "$ENVIRONMENT_PATH" == "/home/ec2-user/environment" ] && grep "alias python=python27" "$UNIX_USER_HOME"/.bashrc; then

    cat <<'EOF' > "$UNIX_USER_HOME"/.bashrc
# .bashrc

export PATH=$PATH:$HOME/.local/bin:$HOME/bin

# load nvm
export NVM_DIR="$HOME/.nvm"
[ "$BASH_VERSION" ] && npm() {
    # hack: avoid slow npm sanity check in nvm
    if [ "$*" == "config get prefix" ]; then which node | sed "s/bin\/node//";
    else $(which npm) "$@"; fi
}
# [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm
rvm_silence_path_mismatch_check_flag=1 # prevent rvm complaints that nvm is first in PATH
unset npm # end hack


# User specific aliases and functions
alias python=python27

# modifications needed only in interactive mode
if [ "$PS1" != "" ]; then
    # Set default editor for git
    git config --global core.editor nano

    # Turn on checkwinsize
    shopt -s checkwinsize

    # keep more history
    shopt -s histappend
    export HISTSIZE=100000
    export HISTFILESIZE=100000
    export PROMPT_COMMAND="history -a;"

    # Source for Git PS1 function
    if ! type -t __git_ps1 && [ -e "/usr/share/git-core/contrib/completion/git-prompt.sh" ]; then
        . /usr/share/git-core/contrib/completion/git-prompt.sh
    fi

    # Cloud9 default prompt
    _cloud9_prompt_user() {
        if [ "$C9_USER" = root ]; then
            echo "$USER"
        else
            echo "$C9_USER"
        fi
    }

    PS1='\[\033[01;32m\]$(_cloud9_prompt_user)\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$(__git_ps1 " (%s)" 2>/dev/null) $ '
fi

EOF

    chown "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/.bashrc
fi

if [ "$ENVIRONMENT_PATH" == "/home/ec2-user/environment" ] && [ ! -f "$ENVIRONMENT_PATH"/README.md ]; then
    cat <<'EOF' >> "$ENVIRONMENT_PATH"/README.md
         ___        ______     ____ _                 _  ___
        / \ \      / / ___|   / ___| | ___  _   _  __| |/ _ \
       / _ \ \ /\ / /\___ \  | |   | |/ _ \| | | |/ _` | (_) |
      / ___ \ V  V /  ___) | | |___| | (_) | |_| | (_| |\__, |
     /_/   \_\_/\_/  |____/   \____|_|\___/ \__,_|\__,_|  /_/
 -----------------------------------------------------------------


Hi there! Welcome to AWS Cloud9!

To get started, create some files, play with the terminal,
or visit https://docs.aws.amazon.com/console/cloud9/ for our documentation.

Happy coding!

EOF

    chown "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/environment/README.md
fi

# Fix for permission error when trying to call `gem install`
chown "$UNIX_USER" -R /usr/local/rvm/gems

#This script is appended to another bash script, so it does not need a bash script file header.

UNIX_USER_HOME="/home/ec2-user"

C9_DIR=$UNIX_USER_HOME/.c9
CONFIG_FILE_PATH="$C9_DIR"/autoshutdown-configuration
VFS_CHECK_FILE_PATH="$C9_DIR"/stop-if-inactive.sh

echo "SHUTDOWN_TIMEOUT=30" > "$CONFIG_FILE_PATH"
chmod a+w "$CONFIG_FILE_PATH"

echo -e '#!/bin/bash
set -euo pipefail
CONFIG=$(cat '$CONFIG_FILE_PATH')
SHUTDOWN_TIMEOUT=${CONFIG#*=}
if ! [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]*$ ]]; then
    echo "shutdown timeout is invalid"
    exit 1
fi
is_shutting_down() {
    is_shutting_down_ubuntu &> /dev/null || is_shutting_down_al1 &> /dev/null || is_shutting_down_al2 &> /dev/null
}
is_shutting_down_ubuntu() {
    local TIMEOUT
    TIMEOUT=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager ScheduledShutdown)
    if [ "$?" -ne "0" ]; then
        return 1
    fi
    if [ "$(echo $TIMEOUT | awk "{print \$3}")" == "0" ]; then
        return 1
    else
        return 0
    fi
}
is_shutting_down_al1() {
    pgrep shutdown
}
is_shutting_down_al2() {
    local FILE
    FILE=/run/systemd/shutdown/scheduled
    if [[ -f "$FILE" ]]; then
        return 0
    else
        return 1
    fi
}
is_vfs_connected() {
    pgrep -f vfs-worker >/dev/null
}

if is_shutting_down; then
    if [[ ! $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] || is_vfs_connected; then
        sudo shutdown -c
    fi
else
    if [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] && ! is_vfs_connected; then
        sudo shutdown -h $SHUTDOWN_TIMEOUT
    fi
fi' > "$VFS_CHECK_FILE_PATH"

chmod +x "$VFS_CHECK_FILE_PATH"

echo "* * * * * root $VFS_CHECK_FILE_PATH" > /etc/cron.d/c9-automatic-shutdown

大まかに以下が行われていることが読み取れます。

  • セキュリティパッチの適用
  • SSH 鍵の追加
  • 自動シャットダウンの許可
  • /home/ec2-user/enviromentへの README.md の作成
  • /usr/local/rvm/gems の所有者の変更
  • 自動シャットダウンスクリプトの設定

シャットダウンスクリプトの確認

Cloud9 環境から、上記のユーザーデータで作成されたスクリプトを確認してみます。

/home/ec2-user/.c9/autoshutdown-configuration

SHUTDOWN_TIME=30

↑この設定を確認した Cloud9 環境は、自動停止期間をデフォルトの 30 分で設定しています。Cloud9 環境作成時に指定したパラメータにより、上記の値は変動するかと思います。

/home/ec2-user/.c9/stop-if-inactive.sh

#!/bin/bash
set -euo pipefail
CONFIG=$(cat /home/ec2-user/.c9/autoshutdown-configuration)
SHUTDOWN_TIMEOUT=${CONFIG#*=}
if ! [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]*$ ]]; then
    echo "shutdown timeout is invalid"
    exit 1
fi
is_shutting_down() {
    is_shutting_down_ubuntu &> /dev/null || is_shutting_down_al1 &> /dev/null || is_shutting_down_al2 &> /dev/null
}
is_shutting_down_ubuntu() {
    local TIMEOUT
    TIMEOUT=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager ScheduledShutdown)
    if [ "$?" -ne "0" ]; then
        return 1
    fi
    if [ "$(echo $TIMEOUT | awk "{print \$3}")" == "0" ]; then
        return 1
    else
        return 0
    fi
}
is_shutting_down_al1() {
    pgrep shutdown
}
is_shutting_down_al2() {
    local FILE
    FILE=/run/systemd/shutdown/scheduled
    if [[ -f "$FILE" ]]; then
        return 0
    else
        return 1
    fi
}
is_vfs_connected() {
    pgrep -f vfs-worker >/dev/null
}

if is_shutting_down; then
    if [[ ! $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] || is_vfs_connected; then
        sudo shutdown -c
    fi
else
    if [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] && ! is_vfs_connected; then
        sudo shutdown -h $SHUTDOWN_TIMEOUT
    fi
fi

Cloud9 環境は一定時間操作しない場合に自動で停止する設定が可能ですが、このような形で実現されているのか、というのが確認できて面白いですね。

終わりに

Cloud9 環境作成時に付随して作成される CloudFormation テンプレートの中身を確認してみました。

裏側でよしなにやってくれるのはとても便利ですが、どういったリソースが作成されているかを押さえておくのも大事かと思います。

いざという時に困らないよういろんなことを気にしておきましょう。

以上、 チバユキ (@batchicchi) がお送りしました。