こんにちは。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の姿勢を私も見習っていきたいです。
この記事が少しでもどなたかのお役に立てば幸いです。