Cloud9をプライベートサブネットに配置する場合のCFnテンプレートを作成してみた

2024.02.14

こんにちは。AWS事業本部コンサルティング部の戸川です。

先日、ちょっと私用で「インターネット接続可能なAWS Cloud9をプライベートサブネットに配置しAWS Systems Manager経由で接続する環境を、VPCごと作ったり消したりする」必要に駆られたので、AWS CloudFormationのテンプレートを作成しました。
せっかくなので、ブログにまとめておこうと思います。

環境構成図

今回CFnで構築する環境は以下になります。

注意点

今回はCloud9をプライベートサブネット上に構築する必要があり、尚且つインターネットとの通信を行いたかったためNATゲートウェイを作成しています。
しかし、NATゲートウェイは維持している時間分の費用とデータ転送量に応じた料金が発生します。要件によっては別の構成を検討するのが良いでしょう。

例えば

  • Cloud9をプライベートサブネットに配置したいけどインターネット通信は必要ない場合:VPCエンドポイント経由で接続する
  • そもそもCloud9をプライベートサブネットに配置する必要が無い場合:Cloud9をパブリックサブネットに配置する
  • など。

    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の姿勢を私も見習っていきたいです。

    この記事が少しでもどなたかのお役に立てば幸いです。