はじめに
RaiseTechのAWSフルコースにて学習した結果、ひとしきり課題どおりの成果物ができあがったこともあり、備忘録ベースで記事として残しておこうと思います。
こちらの記事では、Webアプリケーションを動作させるサーバ構成を、AWS上に構築するところまでを行っていきます。
あらかじめ作成したCloudFormationテンプレートを元に、AWS-CLIのコマンドでスタック作成させるところまでが一旦のゴールですね。
(Ansibleを用いたサーバ上の環境構築・デプロイは 別途記事を作成します 文章を書くのがド下手クソで嫌いなのに、「エンジニア志望でアウトプットしないなんてカスだ」って風潮に流されて嫌々記事を書いた結果、精神的に痛手を負っただけで終わったので、続きは断固書きません。
リポジトリは一応公開してるので、「どうせ環境違えば動かないヘボPlayBookだろうから読んであざ笑ってやろう」って方はどうぞ)
※ 一旦は目標通りに作ることが出来た……という段階であり、ベストプラクティスと呼ぶにはいろいろ内容が足りてないと思います。ご留意。
どんな環境が出来上がるのか
今回のテンプレート(GitHubのリポジトリ)で作成する環境は、下の画像のとおりとなります。
Webアプリケーションが動作するEC2(Amazon Linux2)がアベイラビリティゾーン(A・C)ごとに1台ずつ・RDS(データベース・MySQL)・S3(静的ファイル保管)・外部ネットワークからのアクセスはELBを経由、と最小限の構成を組んでいきます。
※ draw.ioでAWSのインフラ構成図を書く - Qiitaを参考に作成しました。
構築した環境で動作するアプリ
実際に構築したアプリは、GitHubのリポジトリに作成してあります。
以下の記事を参考に作りました。__アプリが動く環境構築の方がメイン__なので、元記事の内容から手を加えた部分はさほど多くありません。
S3の活用=静的ファイル(画像)をアップロードする機能を追加した程度、でしょうか。
作業環境・バージョン
- macOS Big Sur 11.3
- AWS-CLI 2.1.38
CloudFormationテンプレートの内容
入力パラメータの設定
AWSTemplateFormatVersion: 2010-09-09
### … 略
Parameters:
StackEnv:
Description: Stack Environment
Type: String
CidrBlock:
Description: IP range
Type: String
Default: 10.0.0.0/16
EC2ImageId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
KeyName:
Description: The EC2 Key Pair to allow SSH access to the instance
Type: AWS::EC2::KeyPair::KeyName
MyIP:
Description: IP address allowed to access EC2
Type: String
InstanceType:
Description: EC2 Instance Type
Type: String
Default: t2.micro
DBUser:
Description: RDS master user name
Type: String
DBPassword:
Description: RDS master user password
Type: String
NoEcho: true
DBInstanceClass:
Description: DB Instance Class
Type: String
Default: db.t2.micro
DBCharSetCode:
Description: DB Character Set Code
Type: String
Default: utf8mb4
ポイントとしては、
- リソース名にスタック名:
AWS::StackName
とともに作成した環境を明記するため、StackEnv
で環境(本番:prod、開発:devなど)をパラメータとして保持 - 複数のリソースに割り当てることになる
CidrBlock
やInstanceType
、DBCharSetCode
はDefaultで値を設定して各所で使う。念のため、参照が一回のみだがDBInstanceClass
も。 - テンプレートにべた書きはよろしくない認証情報(キーペア名:
KeyName
、DBユーザー名・パスワード:DBUser
,DBPassword
)や、ローカル環境のグローバルIP:MyIP
はパラメータでのみ指定。- パスワードは
NoEcho: true
で表示をマスクしておく。パラメータストアなりSecrets Managerなりを使う方がより適切ですが、一旦動かせることの確認を優先で。
- パスワードは
- EC2のイメージID:
EC2ImageId
は、特にこだわりがなければAWS公式ブログの例どおりに、パラメータストアを使用して最新のAMIを指定する。 - AWS固有のパラメータタイプがある場合はなるべく使う。エディタにLinterが入ってると、Stringから置き換えられる箇所にWarningを出してくれるので、活用したいところですね。
これに加えて、俺的CloudFormation テンプレートの便利な定義 3選 - サーバーワークスエンジニアブログを元に、MetadataにAWS::CloudFormation::Interface
のParameterGroups
を定義して、ラベル付きのグループ分けをしておくと分かりやすいでしょう。
AWSTemplateFormatVersion: 2010-09-09
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
Default: VPC config
Parameters:
- StackEnv
- CidrBlock
-
Label:
Default: EC2 config
Parameters:
- EC2ImageId
- KeyName
- MyIP
- InstanceType
-
Label:
Default: RDS Config
Parameters:
- DBUser
- DBPassword
- DBInstanceClass
- DBCharSetCode
Parameters:
### … 略
といっても、このグループ分けが影響するのはWebコンソール画面上のグルーピングや順序付けとなります。
コマンドラインでスタック作成する分には、Metadataより都度コメントを差し込む方が手間ではないかもしれません。
リソース定義
VPC・サブネット
AWSTemplateFormatVersion: 2010-09-09
Metadata:
### … 略
Parameters:
### … 略
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref CidrBlock
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-vpc
- Key: Env
Value: !Ref StackEnv
# InternetGateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-igw
- Key: Env
Value: !Ref StackEnv
# RouteTable
PublicRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-public-rtb00
- Key: Env
Value: !Ref StackEnv
PublicRouteTableC:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-public-rtb01
- Key: Env
Value: !Ref StackEnv
PrivateRouteTableA:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-private-rtb00
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
PrivateRouteTableC:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-private-rtb01
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
# GatewayAttachment
GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Route
RouteA:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref PublicRouteTableA
GatewayId: !Ref InternetGateway
DependsOn: GatewayAttachment
RouteC:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref PublicRouteTableC
GatewayId: !Ref InternetGateway
DependsOn: GatewayAttachment
# Subnet
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Select [ 0, !Cidr [ !GetAtt VPC.CidrBlock, 1, 8 ]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-public-subnet00
- Key: Env
Value: !Ref StackEnv
PublicSubnetC:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Select [ 1, !Cidr [ !GetAtt VPC.CidrBlock, 2, 8 ]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-public-subnet01
- Key: Env
Value: !Ref StackEnv
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Select [ 2, !Cidr [ !GetAtt VPC.CidrBlock, 3, 8 ]]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-private-subnet00
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
PrivateSubnetC:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Select [ 3, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-private-subnet01
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
# SubnetRouteTableAssociation
SubnetRouteTableAssociation1a:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTableA
SubnetRouteTableAssociation1c:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetC
RouteTableId: !Ref PublicRouteTableC
SubnetRouteTableAssociation1aPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTableA
SubnetId: !Ref PrivateSubnetA
SubnetRouteTableAssociation1cPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTableC
SubnetId: !Ref PrivateSubnetC
以降のリソースも含めて、私の場合は動作確認のためにWebコンソール上で作成した環境とAWS resource and property types reference - AWS CloudFormationのリソースタイプごとの記載を照らし合わせて、自作環境の再現を目標にして記述していきました。
- VPC:
AWS::EC2::VPC
- 付随するインターネットゲートウェイ:
AWS::EC2::InternetGateway
・ルートテーブル:AWS::EC2::RouteTable
- AZごとにサブネット
AWS::EC2::Subnet
をパブリック・プライベートで一種ずつ- アベイラビリティゾーンは、
Fn::GetAZs
・AWS::Region
で利用できるAZを取得して指定する
- アベイラビリティゾーンは、
- リソース間の繋ぎこみを行う
- VPC・インターネットゲートウェイを
AWS::EC2::VPCGatewayAttachment
- インターネットゲートウェイ・ルートテーブル(パブリックサブネット用)を
AWS::EC2::Route
-
DependsOn: GatewayAttachment
で、VPCGatewayAttachment
およびその前に作られるインターネットゲートウェイよりも、Route
の作成を後にすることが可能。一度作成したスタックに内容を修正したテンプレートを適用する際に、変更前のリソースを参照してしまうことによるエラーを防ぐことができます
-
- サブネット・ルートテーブルを
AWS::EC2::SubnetRouteTableAssociation
- VPC・インターネットゲートウェイを
VPC以外も含めて、Tags
が設定できるリソースについては、Name
タグにAWS::StackName
とStackEnv
、リソースの性質やタイプを!Sub
で連結して指定します。Env
タグはまんまStackEnv
ですね。
このあたりは、弊社で使っているAWSリソースの命名規則を紹介します | DevelopersIOの規則をそのまま使わせていただいた感じになります。
各種プロパティに関しては、前述の通り作成済みの環境に基づいて設定していきます。
その中でもCidrBlock
は、VPCではパラメータのCidrBlockをそのまま指定し、各サブネットには[小ネタ]「!Cidr」というチョット便利なCloudFormationの組み込み関数 | DevelopersIOの要領で自動生成してしまうのが良いかと。
EC2・起動テンプレート
# EC2
EC2A:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
ImageId: !Ref EC2ImageId
KeyName: !Ref KeyName
InstanceType: !Ref InstanceType
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: "0"
SubnetId: !Ref PublicSubnetA
GroupSet:
- !Ref SecurityGroupEC2
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-app00
- Key: Env
Value: !Ref StackEnv
IamInstanceProfile: !Ref InstanceProfileEC2
EC2C:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref AWS::Region
ImageId: !Ref EC2ImageId
KeyName: !Ref KeyName
InstanceType: !Ref InstanceType
NetworkInterfaces:
- AssociatePublicIpAddress: true
DeviceIndex: "0"
SubnetId: !Ref PublicSubnetC
GroupSet:
- !Ref SecurityGroupEC2
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-app01
- Key: Env
Value: !Ref StackEnv
IamInstanceProfile: !Ref InstanceProfileEC2
EC2に関しても、原則作成・動作確認済みの環境に基づいた内容を設定します。
ImageId
、KeyName
、InstanceType
はパラメータを参照。今回は2台ともに同じ値を設定するので、まとめてパラメータで管理しております。
NetworkInterfaces - GroupSet
はセキュリティグループ、IamInstanceProfile
はS3アクセス用のIAMロールを当てたインスタンスプロファイルを設定するので、別途設定したリソースの論理IDを!Ref
で渡しておきましょう。
必要であれば、AWS::EC2::Instance
のUserData
プロパティや、AWS::EC2::LaunchTemplate
で作成した起動テンプレートに、EC2起動時に環境構築するコマンドなどを登録・実行させることもできます。
(CloudFormationの中のEC2のユーザーデータでシェル変数を使用する | DevelopersIOなどを参考にするとよいと思います)
ユーザーデータに関しては、このテンプレートでも当初は使っていましたが、
-
yum update
を実行していたが、Ansibleでyum-cronによる自動更新を設定したので必要なくなった - Python3をインストールしてAnsibleを実行させようとしたものの、Amazon Linux2・yumの環境ではPython2でないと動かしづらいため断念
……と、前もって振っておいた作業の必要性がなくなり、さらに環境構築を全面的にAnsibleに任せたこともあって削除しました。
セキュリティグループ
# SecurityGroup
SecurityGroupEC2:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EC2 Security Group
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref SecurityGroupELB
FromPort: 80
ToPort: 80
IpProtocol: tcp
- CidrIp: !Ref MyIP
FromPort: 22
ToPort: 22
IpProtocol: tcp
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
IpProtocol: "-1"
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-app-sg
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
SecurityGroupELB:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ELB Security Group
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: 80
ToPort: 80
IpProtocol: tcp
- CidrIp: 0.0.0.0/0
FromPort: 443
ToPort: 443
IpProtocol: tcp
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
IpProtocol: "-1"
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-elb-sg
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
SecurityGroupRDS:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: RDS Security Group
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref SecurityGroupEC2
FromPort: 3306
ToPort: 3306
IpProtocol: tcp
SecurityGroupEgress:
- DestinationSecurityGroupId: !Ref SecurityGroupEC2
FromPort: 3306
ToPort: 3306
IpProtocol: tcp
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-rds-sg
- Key: Env
Value: !Ref StackEnv
VpcId: !Ref VPC
EC2・ELB・RDSにそれぞれセキュリティグループを設定、SecurityGroupIngress
でインバウンドルールを、SecurityGroupEgress
でアウトバウンドルールを指定していきます。
インバウンド元/アウトバウンド先には、CidrIp
でIPアドレスを指定するだけではなく、SourceSecurityGroupId / DestinationSecurityGroupId
で別のセキュリティグループを指定するとかなり楽に書けるでしょう。
EC2のインバウンドで使用するポート番号については、ELBのリソースのPort
で指定している"80"(HTTP)と、パラメータのMyIP
で設定しているローカル環境から接続する際の"22"(SSH)となります。
(この後でAnsibleを動作させる際に、別途AWS環境内にAnsible稼働用のサーバーを作った方がよいところを、ローカルPCでAnsibleを走らせてSSH接続したEC2の環境構築を行うようにしているので、SSHも必須としてます)
また、ELBのインバウンドにポート番号443(HTTPS)も指定していますが、別途SSL接続の設定をできているわけではないので、今のところ役立てられてはいません。
いずれ活用したいところです。
ELB・ターゲットグループ・リスナー
# LoadBalancer
ElasticLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: access_logs.s3.enabled
Value: "false"
- Key: deletion_protection.enabled
Value: "false"
- Key: idle_timeout.timeout_seconds
Value: "60"
- Key: routing.http.desync_mitigation_mode
Value: "defensive"
- Key: routing.http.drop_invalid_header_fields.enabled
Value: "false"
- Key: routing.http2.enabled
Value: "true"
Name: !Sub ${AWS::StackName}-${StackEnv}-elb
Scheme: internet-facing
SecurityGroups:
- !Ref SecurityGroupELB
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-elb
- Key: Env
Value: !Ref StackEnv
Type: application
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
Matcher:
HttpCode: "200"
Name: !Sub ${AWS::StackName}-${StackEnv}-tg
Port: 80
Protocol: HTTP
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-tg
- Key: Env
Value: !Ref StackEnv
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: "300"
- Key: stickiness.enabled
Value: "false"
- Key: load_balancing.algorithm.type
Value: "round_robin"
- Key: slow_start.duration_seconds
Value: "0"
Targets:
- Id: !Ref EC2A
Port: 80
- Id: !Ref EC2C
Port: 80
TargetType: instance
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref ElasticLoadBalancer
Port: 80
Protocol: HTTP
今回作成するのはALBなので、AWS::ElasticLoadBalancingV2::LoadBalancer
、AWS::ElasticLoadBalancingV2::TargetGroup
、AWS::ElasticLoadBalancingV2::Listener
を使って正常なプロパティを指定していきます。
ロードバランサーに限りませんが、事前に手作業で動作する環境を作ってイメージを持っておくのが近道でしょうか。いきなりテンプレートからリソースを作成、というのは初心者には厳しいかと。
前述のとおりSSLの設定はしていないので、あくまでHTTPのみでの接続となります。
RDB・パラメータグループ・サブネットグループ
# RDS
RdsMysql:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: "20"
AllowMajorVersionUpgrade: true
AutoMinorVersionUpgrade: true
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
BackupRetentionPeriod: 7
CopyTagsToSnapshot: true
DBInstanceClass: !Ref DBInstanceClass
DBInstanceIdentifier: !Sub ${AWS::StackName}-${StackEnv}-rds
DBParameterGroupName: !Ref DBParameterGroup
DBSubnetGroupName: !Ref DBSubnetGroup
DeletionProtection: false
EnablePerformanceInsights: false
Engine: mysql
EngineVersion: 8.0.20
LicenseModel: general-public-license
MasterUserPassword: !Ref DBPassword
MasterUsername: !Ref DBUser
MaxAllocatedStorage: 1000
MonitoringInterval: 0
MultiAZ: false
Port: "3306"
PreferredBackupWindow: 18:00-18:30
PreferredMaintenanceWindow: mon:03:00-mon:03:30
PubliclyAccessible: false
StorageEncrypted: false
StorageType: gp2
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-rds
- Key: Env
Value: !Ref StackEnv
VPCSecurityGroups:
- !Ref SecurityGroupRDS
# DBParameterGroup
DBParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: sample parameter cloudformation
Family: mysql8.0
Parameters:
character_set_client: !Ref DBCharSetCode
character_set_connection: !Ref DBCharSetCode
character_set_database: !Ref DBCharSetCode
character_set_results: !Ref DBCharSetCode
character_set_server: !Ref DBCharSetCode
collation_server: utf8mb4_general_ci
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-rds-pg
- Key: Env
Value: !Ref StackEnv
# DBSubnetGroup
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: subnetgroup created by cloudformation
DBSubnetGroupName: !Sub ${AWS::StackName}-${StackEnv}-rds-subnetg
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-rds-subnetg
- Key: Env
Value: !Ref StackEnv
かなり長い内容ではありますが、各プロパティが必須であるかないかをあまり重要視せず、作成済みの環境の設定内容をそのままテンプレートに適用した形となっております。
利用額は低く収めておきたいので、マルチAZなどは設定せず。
今回のエンジンはMySQL、バージョンは8.0.20を使用しました。
AWS::RDS::DBInstance - EngineVersion
はもちろん、AWS::RDS::DBParameterGroup - Family
にも同じバージョンを指定しておきましょう。
初期パラメータについては、character関連にパラメータDBCharSetCode
を参照したのと、collation_server
にひらがなカタカナを判別する日本語用Collationであるutf8mb4_ja_0900_as_cs_ks
を設定……したものの、AWSのパラメータグループはバージョン8.0止まりのためか、8.0.1で導入されたCollationはサポートされておらず。
やむなくutf8mb4_general_ci
に変更しました。
AWS::RDS::DBSubnetGroup
でRDSのサブネットグループを作成するのですが、グループには必ず2つ以上のAZが含まれていなければいけません。
ここで、あらかじめ作成したAZ-AとAZ-Cのプライベートサブネットを参照しておきます。
S3・アクセス用IAMロール
# S3 Bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
AccelerateConfiguration:
AccelerationStatus: Suspended
AccessControl: Private
BucketName: !Sub ${AWS::StackName}-${StackEnv}-contents-${AWS::AccountId}
ObjectLockEnabled: false
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-contents-${AWS::AccountId}
- Key: Env
Value: !Ref StackEnv
VersioningConfiguration:
Status: Suspended
今回作成するS3については、原則EC2上のアプリケーションで取り扱う静的ファイルを保存するのみなので、基本プライベートでパブリックアクセスもブロックする設定にしておきます。
気をつけるところがあるとすれば、BucketName
プロパティの付け方次第ではバケット名の制約に引っかかってしまう点でしょうか。
今回はAWS::AccountId
でアカウントIDを追記しておき、他ユーザーのバケット名とバッティングする可能性を低くしておきました。
一応必須のプロパティではないので、テンプレート上で命名せずにOutputs
でバケット名を出力して確認できるようにしておくのもよいかもしれません。
そして、EC2からS3へのアクセスが行えるよう、IAMロールとインスタンスプロファイルも以下で作成しておきます。
# IAM Role
RoleEC2:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Description: Allows EC2 instances to call AWS services on your behalf.
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonS3FullAccess
MaxSessionDuration: 3600
Path: /
RoleName: !Sub ${AWS::StackName}-${StackEnv}-app-role
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-${StackEnv}-app-role
- Key: Env
Value: !Ref StackEnv
# IAM InstanceProfile
InstanceProfileEC2:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub ${AWS::StackName}-${StackEnv}-app-ip
Path: /
Roles:
- !Ref RoleEC2
公式ドキュメントでCloudFormation で IAM マネージドポリシーを IAM ロールにアタッチする方法が解説されているので、同様にAWS::IAM::Role
でロールを作成。
セキュリティ上のベストは、テンプレート中で作成したS3バケットのみへのアクセスを許可したポリシーをAWS::IAM::ManagedPolicy
で設定するべきですが、今回は手っ取り早くAWS管理ポリシー(AmazonS3FullAccess
)を使用。
作成したロールはAWS::IAM::InstanceProfile
で作成したインスタンスプロファイルへ割り当てて、そのプロファイルをEC2のIamInstanceProfile
プロパティで参照すれば、アクセス許可の付与は完了します。
# EC2
EC2A:
Type: AWS::EC2::Instance
Properties:
### … 略
IamInstanceProfile: !Ref InstanceProfileEC2
EC2C:
Type: AWS::EC2::Instance
Properties:
### … 略
IamInstanceProfile: !Ref InstanceProfileEC2
※ この他にもOutputs
セクションで、作成したリソースの値などを出力してコンソールやコマンドラインで受け取れるように設定できますが、今回は使用していません。
コマンドラインでスタック作成
公式ドキュメントの使っているOSのページを元に、AWS CLIバージョン2をインストール。
aws cloudformation create-stack
を使ってもいいのですが、aws cloudformation deploy
であれば、スタックの新規作成も既存スタック更新もできるので、そちらで問題ないでしょう。
Jenkinsなどで継続的に実行するつもりなら、最初からaws cloudformation deploy
でジョブを登録した方が楽かと。
aws cloudformation deploy --template-file テンプレート名 \
--stack-name スタック名 --capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides KeyName=キーペア名 MyIP=ローカル環境のパブリックIP \
DBUser=DBユーザー名 DBPassword=DBパスワード StackEnv=環境(prod/stg/devなど)
今回のテンプレートではIAMロールを作成するので、--capabilities
オプションでCAPABILITY_NAMED_IAM
を指定しないとエラーとなります。
最後に
RaiseTechのメンターの方からのアドバイスもあって、やってきたことを記事として残してみました。
結果、改めて自分の起こしたコードを念入りに読み返すことで、「もっといいやり方があるのではないか?」といった気づきができたのはプラスではあるな、と思いました。
(第三者が読んでも、読みやすい文章になっている保証は全然できませんが……)
そして、この記事ではWebアプリケーションを動かすサーバー構成の構築にとどまっているので、以降のAnsibleでのデプロイについては別途記事を書こうかと思います。
書きません。