
Cloud9をプライベートサブネットに配置する場合のCFnテンプレートを作成してみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは。AWS事業本部コンサルティング部の戸川です。
先日、ちょっと私用で「インターネット接続可能なAWS Cloud9をプライベートサブネットに配置しAWS Systems Manager経由で接続する環境を、VPCごと作ったり消したりする」必要に駆られたので、AWS CloudFormationのテンプレートを作成しました。
せっかくなので、ブログにまとめておこうと思います。
環境構成図
今回CFnで構築する環境は以下になります。

注意点
今回はCloud9をプライベートサブネット上に構築する必要があり、尚且つインターネットとの通信を行いたかったためNATゲートウェイを作成しています。
しかし、NATゲートウェイは維持している時間分の費用とデータ転送量に応じた料金が発生します。要件によっては別の構成を検討するのが良いでしょう。
例えば
など。
CFnテンプレート
テンプレート構成
├── cloud9 │ └── cloud9.yaml ├── codecommit │ └── codecommit.yaml ├── iam │ └── iam.yaml ├── subnet │ ├── private │ │ └── subnet.yaml │ └── public │ └── subnet.yaml │── vpc │ └── vpc.yaml └── main.yaml
main.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: main stack
Parameters:
 CreateRepository:
  Default: "yes"
  Type: String
  AllowedValues: ["yes", "no"]
Conditions:
  CreateRepository: !Equals
    - !Ref CreateRepository
    - "yes"
Resources:
  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: vpc/vpc.yaml
  PrivateSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: subnet/private/subnet.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID
        NatGateway: !GetAtt PublicSubnet.Outputs.NatGatewayID
  PublicSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: subnet/public/subnet.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID
  IAM:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: iam/iam.yaml
  CodeCommit:
    Type: AWS::CloudFormation::Stack
    Condition: CreateRepository
    Properties:
      TemplateURL: codecommit/codecommit.yaml
  Cloud9:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: cloud9/cloud9.yaml
      Parameters:
        Subnet: !GetAtt PrivateSubnet.Outputs.SubnetID
補足
- スタック削除時にCodeCommitリポジトリが削除されないよう、Retainオプションを指定しています
- その結果、同アカウントで再度スタックを作成する場合には同じ名前のリポジトリが既に存在するというエラーが発生します
- そのためテンプレートのConditionsセクションにて、スタック作成時にCodeCommitも併せて作成するか否かを選択できるようにしています
- CodeCommitを作成しない場合はdeployコマンドに--parameter-overrides CreateRepository="no"を付加します
vpc/vpc.yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EnvironmentName:
    Type: String
    Default: work-cloud9
  VpcCIDR:
    Type: String
    Default: 10.192.0.0/16
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
Outputs:
  VPCID:
    Value: !Ref VPC
subnet/private/subnet.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "private subnet"
Parameters:
  VPC:
    Type: AWS::EC2::VPC::Id
  PrivateSubnetCIDR:
    Type: String
    Default: 10.192.20.0/24
  NatGateway:
    Type: String
Resources:
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnetCIDR
      MapPublicIpOnLaunch: false
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: Private Routes
  DefaultPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet
Outputs:
  SubnetID: 
    Value: !Ref PrivateSubnet
subnet/public/subnet.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "public subnet"
Parameters:
  VPC:
    Type: AWS::EC2::VPC::Id
  PublicSubnetCIDR:
    Type: String
    Default: 10.192.10.0/24
Resources:
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnetCIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: Public Subnet
  InternetGateway:
    Type: AWS::EC2::InternetGateway
  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet
  NoIngressSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "no-ingress-sg"
      GroupDescription: "Security group with no ingress rule"
      VpcId: !Ref VPC
Outputs:
  SubnetID:
    Value: !Ref PublicSubnet
  NatGatewayID:
    Value: !Ref NatGateway
iam/iam.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  IAMRoleforCloud9:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AWSCloud9SSMAccessRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
                - cloud9.amazonaws.com
            Action:
              - "sts:AssumeRole"
      MaxSessionDuration: 3600
      Path: "/service-role/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloud9SSMInstanceProfile
  InstanceProfileforCloud9:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: AWSCloud9SSMInstanceProfile
      Path: /cloud9/
      Roles:
        - !Ref IAMRoleforCloud9
  IAMPolicyforCloud9:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Policy for Cloud9
      Path: /
      ManagedPolicyName: Cloud9-IAM-Policy1
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: ecr:GetAuthorizationToken
            Resource: "*"
          - Effect: Allow
            Action: s3:GetObject
            Resource: "*"
          - Effect: Allow
            Action: codecommit:GitPull
            Resource: "*"
      Roles:
        - !Ref IAMRoleforCloud9
codecommit/codecommit.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  CodeCommit:
    Type: AWS::CodeCommit::Repository
    DeletionPolicy: Retain
    Properties:
      RepositoryName: work-repo
      RepositoryDescription: Work Repo for Cloud9
補足
- スタック削除時にリポジトリが削除されないようRetainオプションを指定しています
cloud9/cloud9.yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EnvironmentName:
    Type: String
    Default: work-cloud9
  AutomaticStopTimeMinutes:
    Type: Number
    Default: 30
  ConnectionType:
    Type: String
    Default: CONNECT_SSM
  ImageId:
    Type: String
    Default: resolve:ssm:/aws/service/cloud9/amis/amazonlinux-2023-x86_64
  InstanceType:
    Type: String
    Default: t3.micro
  OwnerArn:
    Type: String
    Default: <ENVIRONMENT_OWNER_ARN>
  Subnet:
    Type: String
Resources:
  Cloud9:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      AutomaticStopTimeMinutes: !Ref AutomaticStopTimeMinutes
      ConnectionType: !Ref ConnectionType
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      Name: !Ref EnvironmentName
      OwnerArn: !Ref OwnerArn
      SubnetId: !Ref Subnet
      Repositories:
        - RepositoryUrl: https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/work-repo
          PathComponent: codecommit/work-repo
補足
- ENVIRONMENT_OWNER_ARNの部分はCloud9環境のオーナーにしたいIAMユーザー、ロールのARNを記載してください
- 別アカウントからスイッチロールした先のアカウントでCloud9を構築する場合はarn:aws:sts::スイッチ先アカウント番号:assumed-role/スイッチ先ロール名/セッション名(ユーザー名)になります
スタック作成手順
S3バケットへのテンプレートアップロードとartifact.yamlの作成
AWS CLIが使用可能な環境で以下のコマンドを実行します。
aws cloudformation package --template-file main.yaml --s3-bucket テンプレート格納用S3バケット名 --output-template-file artifact.yaml
main.yaml内の各テンプレートファイルへの参照を、S3 URLへ置換したartifact.yamlが作成されます。
スタックの作成
次に以下のコマンドでスタックを作成します。
aws cloudformation deploy --template-file artifact.yaml --stack-name cloud9-construction スタック名 --capabilities CAPABILITY_NAMED_IAM
なお、前述の通りCodeCommitが既に作成済の場合は--parameter-overrides CreateRepository="no"を付与します。
最後に
インプットした分だけアウトプットしてくれる、そんなCloudFormationの姿勢を私も見習っていきたいです。
この記事が少しでもどなたかのお役に立てば幸いです。










