- 1. 解説動画
- 2. 概要
- ELB (Elastic Load Balancing)
- 構成図
- リソース一覧
- 命名規則
- 公式URL
- 3. 解説手順
- 4. CFnテンプレート
- IAM Access Analyzer
- ELB (ALB)
- EC2
- 5. 予習
- 6. FAQ(随時追加)
ELB (Elastic Load Balancing) について学んでいきましょう!
1. 解説動画
※動画の画質が悪い場合は、歯車から720p以上の画質を選択ください
2. 概要
ELB (Elastic Load Balancing)
【Load Balancingとは】
Load Balancingとは「負荷分散」という名の通り、クライアントからのリクエストを分散させることを意味します。オンプレの時代は高級な装置で、年間100万円以上するので触れることも勉強することも貴重でした。今はいい時代ですね。F5のBIG-IPやA10 Networksの機器が有名でした。
分散先(=ターゲットと呼ぶ)は当初EC2インスタンスだけでしたが、今ではLambdaやECSコンテナなどにも分散できます。
インターネットに向けて公開するELBと、VPC内部でしか使えない内部ELBの2通りあります。後者は最後の手段で、あまり使わないで済む構成にしたいところです(お金がかかるので...)。
【ELBの種類】
ELBは全部で4種類あります。CLBを除く3種類は、CFnでは ElasticLoadBalancingV2
、AWS CLIでは elbv2
と表記されるのでご注意ください。
些末ながら、ELBのBは「Balancing(=分散するサービス)」で、ALBなどのBは「Balancer(=分散する装置)」で微妙に違うのでご認識を!
比較の詳細は公式の情報 https://aws.amazon.com/jp/elasticloadbalancing/features/ をご覧ください。下記は特に重要な点をまとめました。
ELBの種類 | 使い分け | メリット |
---|---|---|
ALB
(Application Load Balancer) | HTTP/HTTPSに特化 | Lambdaにも振り分けできる。ユーザー認証も実装できる |
NLB
(Network Load Balancer) | 大量のTCP/UDP/TLSトラフィックに対応できる | IPアドレスを固定できる |
GWLB
(Gateway Load Balancer)
※2020年11月に新登場 | サードパーティーのセキュリティ製品を導入する際に便利 | パケットを加工しないため、セキュリティ製品を透過的に挿入できる |
CLB
(Classic Load Balancer) | 昔使われたELB ※今は使わない | EC2-Classic (=VPCがなかった時のEC2) に対応できる |
構成図
ELB(ALB)をPublic Subnet A,C,Dに作ります(=インターネットに公開するためにPublic Subnetに作る必要があります)次にEC2をProtected Subnet C(もしくはPublic Subnet C)に作ります。ALBログ保管用のS3バケットも作ります。
今回作成するALB, EC2の構成図はこちらです。 ※小さくて見づらいので、右クリックして新しいタブで開いてください。
リソース一覧
リソース | 目的 | 備考 |
---|---|---|
AccessAnalyzer: Analyzer | AWSアカウント外からのアクセスを分析する「アナライザー」 | 信頼ゾーン(今回はAWSアカウント)外からのアクセスを許可するポリシーをモニタリングしてくれる |
S3: Bucket | ファイル(S3ではオブジェクトと言う)を保存できる「バケット」 | バケット名は世界中で一意(=唯一)でないといけない。そのためアカウントIDをバケット名に入れている。暗号化は必須ではないが、フラグ一つで暗号化して保存してくれる。Public Access Block を有効にすることで、公開状態になることを確実に回避できる |
S3: BucketPolicy | バケットのアクセス権限を制御するための「バケットポリシー」 | S3のポリシーは様々あるので厄介だが、今回はAWSがALBのログをファイルを出力するために必要。このようにバケットポリシーではAWSアカウント外からのファイル(オブジェクト)操作について許可設定ができる |
EC2: SecurityGroup, SecurityGroupIngress | ELBに対するInbound(Ingress)=内向きOutbound(Outgress)=外向きの通信を許可する「セキュリティグループ」 | 内向きは許可するものだけルールを追加していく。今回はHTTP(TCP 80番ポート)、HTTPS(TCP 443番ポート)をフルオープンにする。 外向きは絞っても良いが、面倒なので通常はデフォルトのフルオープンのままで使うことが多い |
ElasticLoadBalancingV2: LoadBalancer | ELB(負荷分散)を実現するための装置「ロードバランサー」 | ALBを作成する。インターネットに公開するため、Public Subnetで作成する。画面上は1つに見えるが、実際は指定した3つのサブネットに作られる。HTTP/2を有効にする |
ElasticLoadBalancingV2: Listener | ALBがリクエストを受け付けるための「リスナー」 | HTTP(TCP 80番ポート)、HTTPS(TCP 443番ポート)のリスナーを作成する。HTTPSの場合は証明書とSSLポリシーの設定も必要。デフォルトのアクションもあわせて設定する |
ElasticLoadBalancingV2: ListenerRule | リスナーの挙動(条件とアクションのセット)を定義できる「リスナールール」 | 今回は挙動を試すため「302 Found」と「404 Not Found」のリスナールールを追加する |
Route53: RecordSet | Route 53のHostedZone(簡単に言うとドメイン)に追加する「レコードセット」 | ドメイン→IPv4アドレス に変換するためのAレコードを追加する。追加するとブラウザ等から所有するドメインでアクセスできるようになる。今回はALBのエイリアス(別名)のレコードを追加する |
IAM: Role | EC2へ権限付与するための「ロール」 | ManagedPolicyで ①ECRにイメージをPushする権限(ECSの回で説明)と ②Session Managerを使う権限 を付与する |
IAM: InstanceProfile | 「ロール」をEC2にアタッチするために必要な「インスタンスプロファイル」 | CFnでは「インスタンスプロファイル」を「ロール」と別に作る。一方、画面上では明示的に作る必要はなく、EC2のロールを作ると勝手に作られ、設定値の一つとして表示されるだけ |
EC2: Instance | バーチャルマシン(仮想サーバ)=「インスタンス」 | AMIのイメージID、インスタンスタイプ、インスタンスプロファイル、サブネットID、セキュリティグループIDなどを指定して作成する |
ElasticLoadBalancingV2: TargetGroup | ALBの振り分け先(=ターゲット)をまとめるための「ターゲットグループ」 | 今回のターゲットは、HTTP(TCP 80番ポート)が開いているEC2インスタンス。ターゲットグループの設定の中でもヘルスチェック、スティッキーセッションの設定が特に重要 |
命名規則
Classmethodさんの命名規則 https://dev.classmethod.jp/articles/aws-name-rule/ に準拠します。
公式URL
ELB
- Docs: Elastic Load Balancing とは - Elastic Load Balancing
- CFn: ElasticLoadBalancingV2 リソースタイプのリファレンス - AWS CloudFormation
- CLI: elbv2 — AWS CLI v2 Command Reference
3. 解説手順
- CFnでELBのリソースを作成する
- 動作確認を実施する
※Macの場合はターミナルで、Windowsの場合はCloudShellか、EC2にSessionManagerで繋いで実行してください
curl -v http://elb.[ご自身のドメイン名]/
※httpでリクエストcurl -v https://elb.[ご自身のドメイン名]/
※httpsでリクエストcurl -v https://elb.[ご自身のドメイン名]/users
※上でリダイレクトされたパスにリクエストcurl -k -v https://[上記コマンド実行結果 1行目のIPアドレス]/
※ELBのIPアドレスでリクエスト。これだけ-k
オプションをお忘れなく!! ※応答内容はどこかにコピペして保存しておいてください 👉 宿題に出ます - CFnでELBで振り分けるEC2のリソースを作成する
- 動作確認を実施する
※応答内容はどこかにコピペして保存しておいてください 👉 宿題に出ます
※もし
curl -v https://elb.[ご自身のドメイン名]/users
の応答が500番台になる場合は構築時に何か失敗しています。例えばNAT Gatewayを削除された場合はProtected Subnetが使えませんので、使わないように変更ください。CloudFormationで[SystemName]-prod-viaelb-ec2を選択 > Updateボタン > Use current templateを選択 > Nextボタン > UseSubnetProtected: falseを選択 > Nextボタン > Nextボタン > Update Stackボタン
4. CFnテンプレート
IAM Access Analyzer
- システムごとではなく、リージョン共通の設定なので、Stack NameにもFile Nameにも[SystemName]をつけずに作成ください。
Stack Name: access-analyzer File Name: access-analyzer.yml
---
AWSTemplateFormatVersion: "2010-09-09"
Description: Create Access Analyzer
Resources:
## Access Analyzer: Analyzer (External)
ExternalAccessAnalyzer:
Type: AWS::AccessAnalyzer::Analyzer
Properties:
AnalyzerName: ExternalAccessAnalyzer
Type: ACCOUNT
Outputs:
## Access Analyzer: Analyzer (External)
ExternalAccessAnalyzer:
Value: !Ref ExternalAccessAnalyzer
Export:
Name: !Sub ${AWS::StackName}-ExternalAccessAnalyzer
【変更履歴】
- 2024-12-27:新規追加
ELB (ALB)
- S3: Bucket(アクセスログ用), BucketPolicy
- EC2: SecurityGroup, SecurityGroupIngress
- ELBv2: LoadBalancer, Listener, ListenerRule
- Route53: RecordSet
を作成します。
- [SystemName]=
awsmaster
をお好きなシステム名に置換してください。変えなくてもOKです。
Stack Name: [SystemName]-prod-elb File Name: [SystemName]/elb.yml
---
### [Change System Name] awsmaster
## The following CFn stack must be created first in order to be referenced by the ImportValue function.
## 1. ${SystemName}-${Environment}-route53
## 2. ${SystemName}-${Environment}-certificatemanager
## 3. ${SystemName}-${Environment}-vpc
AWSTemplateFormatVersion: "2010-09-09"
Description: Create ELB
Parameters:
SystemName:
Description: System Name
Type: String
Default: awsmaster ### [Change System Name]
Environment:
Description: Environment
Type: String
Default: prod
AllowedValues:
- prod
- stg
- dev
SubDomain:
Description: Sub Domain
Type: String
Default: elb
AllowedPattern: ^[^.]*$
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Environment Configuration"
Parameters:
- SystemName
- Environment
- SubDomain
Resources:
## S3: Bucket (Access Logs)
S3BucketAccessLogs:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-${Environment}-alb-accesslogs-${AWS::AccountId}
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true
## S3: Bucket (Connection Logs)
S3BucketConnectionLogs:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-${Environment}-alb-connectionlogs-${AWS::AccountId}
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true
## S3: Bucket Policy (Access Logs)
S3BucketPolicyAccessLogs:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketAccessLogs
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: s3:PutObject
Principal:
## The account ID for ELB log output is fixed: 582318560864
## Enable access logs for your Application Load Balancer - Elastic Load Balancing
## https://docs.aws.amazon.com/elasticloadbalancing/latest/application/enable-access-logging.html
AWS: arn:aws:iam::582318560864:root
Resource: !Sub ${S3BucketAccessLogs.Arn}/AWSLogs/${AWS::AccountId}/*
## S3: Bucket Policy (Connection Logs)
S3BucketPolicyConnectionLogs:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketConnectionLogs
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: s3:PutObject
Principal:
## The account ID for ELB log output is fixed: 582318560864
## Enable connection logs for your Application Load Balancer - Elastic Load Balancing
## https://docs.aws.amazon.com/elasticloadbalancing/latest/application/enable-connection-logging.html
AWS: arn:aws:iam::582318560864:root
Resource: !Sub ${S3BucketConnectionLogs.Arn}/AWSLogs/${AWS::AccountId}/*
## EC2(VPC): Security Group
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${SystemName}-${Environment}-alb-sg
GroupDescription: !Sub ${SystemName}-${Environment}-alb-sg
VpcId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-VPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-alb-sg
## EC2(VPC): Security Group Inbound Rule From Any (HTTP)
EC2SecurityGroupIngressHttp:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref EC2SecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: any
## EC2(VPC): Security Group Inbound Rule From Any (HTTPS)
EC2SecurityGroupIngressHttps:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref EC2SecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: any
## ELB: Load Balancer (ALB)
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
## Because permission to output access and connection logs is required first when creating ALB.
DependsOn:
- S3BucketPolicyAccessLogs
- S3BucketPolicyConnectionLogs
Properties:
Name: !Sub ${SystemName}-${Environment}-alb
IpAddressType: ipv4
Type: application
Subnets:
- Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-SubnetPublicA
- Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-SubnetPublicC
- Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-SubnetPublicD
LoadBalancerAttributes:
- Key: routing.http2.enabled
Value: true
- Key: deletion_protection.enabled
Value: true
- Key: access_logs.s3.enabled
Value: true
- Key: access_logs.s3.bucket
Value: !Ref S3BucketAccessLogs
- Key: connection_logs.s3.enabled
Value: true
- Key: connection_logs.s3.bucket
Value: !Ref S3BucketConnectionLogs
SecurityGroups:
- !Ref EC2SecurityGroup
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-alb
## ELB: Listener (HTTP)
ListenerHttp:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: "443"
Host: "#{host}"
Path: "/#{path}"
Query: "#{query}"
StatusCode: HTTP_301
## ELB: Listener (HTTPS)
ListenerHttps:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-certificatemanager-Certificate
SslPolicy: ELBSecurityPolicy-TLS13-1-2-Res-2021-06
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: "403"
ContentType: text/html
MessageBody: |
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>
## ELB: Listener Rule (302 Found)
ListenerRuleRedirect:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref ListenerHttps
Priority: 11
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Sub
- "${SubDomain}.${DomainName}"
- DomainName:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-route53-HostedZoneDomainName
- Field: path-pattern
PathPatternConfig:
Values:
- /
Actions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: "443"
Host: "#{host}"
Path: /users
Query: "#{query}"
StatusCode: HTTP_302
## ELB: Listener Rule (404 Not Found)
ListenerRuleNotFound:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref ListenerHttps
Priority: 100
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Sub
- "${SubDomain}.${DomainName}"
- DomainName:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-route53-HostedZoneDomainName
Actions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: "404"
ContentType: text/html
MessageBody: |
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
</body>
</html>
## Route 53: Record Set (IPv4)
Route53RecordSetIPv4:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-route53-HostedZone
Name: !Sub
- "${SubDomain}.${DomainName}."
- DomainName:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-route53-HostedZoneDomainName
Type: A
AliasTarget:
HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
DNSName: !Sub dualstack.${LoadBalancer.DNSName}
Outputs:
## S3: Bucket (Access Logs)
S3BucketAccessLogs:
Value: !Ref S3BucketAccessLogs
Export:
Name: !Sub ${AWS::StackName}-S3BucketAccessLogs
S3BucketAccessLogsArn:
Value: !GetAtt S3BucketAccessLogs.Arn
Export:
Name: !Sub ${AWS::StackName}-S3BucketAccessLogsArn
S3BucketAccessLogsDomainName:
Value: !GetAtt S3BucketAccessLogs.DomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketAccessLogsDomainName
S3BucketAccessLogsDualStackDomainName:
Value: !GetAtt S3BucketAccessLogs.DualStackDomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketAccessLogsDualStackDomainName
S3BucketAccessLogsRegionalDomainName:
Value: !GetAtt S3BucketAccessLogs.RegionalDomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketAccessLogsRegionalDomainName
## S3: Bucket (Connection Logs)
S3BucketConnectionLogs:
Value: !Ref S3BucketConnectionLogs
Export:
Name: !Sub ${AWS::StackName}-S3BucketConnectionLogs
S3BucketConnectionLogsArn:
Value: !GetAtt S3BucketConnectionLogs.Arn
Export:
Name: !Sub ${AWS::StackName}-S3BucketConnectionLogsArn
S3BucketConnectionLogsDomainName:
Value: !GetAtt S3BucketConnectionLogs.DomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketConnectionLogsDomainName
S3BucketConnectionLogsDualStackDomainName:
Value: !GetAtt S3BucketConnectionLogs.DualStackDomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketConnectionLogsDualStackDomainName
S3BucketConnectionLogsRegionalDomainName:
Value: !GetAtt S3BucketConnectionLogs.RegionalDomainName
Export:
Name: !Sub ${AWS::StackName}-S3BucketConnectionLogsRegionalDomainName
## EC2(VPC): Security Group
EC2SecurityGroup:
Value: !Ref EC2SecurityGroup
Export:
Name: !Sub ${AWS::StackName}-EC2SecurityGroup
EC2SecurityGroupVpcId:
Value: !GetAtt EC2SecurityGroup.VpcId
Export:
Name: !Sub ${AWS::StackName}-EC2SecurityGroupVpcId
## ELB: LoadBalancer (ALB)
LoadBalancer:
Value: !Ref LoadBalancer
Export:
Name: !Sub ${AWS::StackName}-LoadBalancer
LoadBalancerCanonicalHostedZoneID:
Value: !GetAtt LoadBalancer.CanonicalHostedZoneID
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerCanonicalHostedZoneID
LoadBalancerDNSName:
Value: !GetAtt LoadBalancer.DNSName
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerDNSName
LoadBalancerFullName:
Value: !GetAtt LoadBalancer.LoadBalancerFullName
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerFullName
LoadBalancerName:
Value: !GetAtt LoadBalancer.LoadBalancerName
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerName
LoadBalancerSecurityGroup1:
Value: !Select [ 0, !GetAtt LoadBalancer.SecurityGroups ]
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerSecurityGroup1
## ELB: Listener (HTTP)
ListenerHttp:
Value: !Ref ListenerHttp
Export:
Name: !Sub ${AWS::StackName}-ListenerHttp
## ELB: Listener (HTTPS)
ListenerHttps:
Value: !Ref ListenerHttps
Export:
Name: !Sub ${AWS::StackName}-ListenerHttps
## ELB: Listener Rule (301 Redirect)
ListenerRuleRedirect:
Value: !Ref ListenerRuleRedirect
Export:
Name: !Sub ${AWS::StackName}-ListenerRuleRedirect
## ELB: Listener Rule (404 Not Found)
ListenerRuleNotFound:
Value: !Ref ListenerRuleNotFound
Export:
Name: !Sub ${AWS::StackName}-ListenerRuleNotFound
## Route 53: Record Set (IPv4)
Route53RecordSetIPv4:
Value: !Ref Route53RecordSetIPv4
Export:
Name: !Sub ${AWS::StackName}-Route53RecordSetIPv4
【変更履歴】
- 2022-04-27:S3バケットで
OwnershipControls
の設定を追加(ACL無効に変更) - 2022-11-30:不要な
WebsiteURL
Outputの削除 - 2023-04-06:冒頭でImportValueについてのコメント補足追加
- 2023-04-14:コメント文を全般的に修正
- 2023-08-04:セキュリティポリシーを
ELBSecurityPolicy-FS-1-2-Res-2020-10
からELBSecurityPolicy-TLS13-1-2-Res-2021-06
に変更、バケットポリシーのコメント文の修正 - 2023-10-07:
PublicAccessBlockConfiguration
の順番を変更(画面に合わせた) - 2025-01-15:接続ログの出力設定と出力先のS3バケットを追加
EC2
- IAM: Role, InstanceProfile
- EC2: SecurityGroup, SecurityGroupIngress(インバウンドルール), Instance
- ELBv2: TargetGroup, ListenerRule
を作成します。
- [SystemName]=
awsmaster
をお好きなシステム名に置換してください。変えなくてもOKです。 - [UseSubnetProtected]=
true
ですが、もし④VPCでNAT Gatewayを削除していたら、必ずfalse
に変更してください。
Stack Name: [SystemName]-prod-viaelb-ec2 File Name: [SystemName]/viaelb/ec2.yml
---
### [Change System Name] awsmaster
## The following CFn stack must be created first in order to be referenced by the ImportValue function.
## 1. ${SystemName}-${Environment}-route53
## 2. ${SystemName}-${Environment}-vpc
## 3. ${SystemName}-${Environment}-rds
## 4. ${SystemName}-${Environment}-elb
AWSTemplateFormatVersion: "2010-09-09"
Description: Create EC2 Instance etc.
Parameters:
SystemName:
Description: System Name
Type: String
Default: awsmaster ### [Change System Name]
Environment:
Description: Environment
Type: String
Default: prod
AllowedValues:
- prod
- stg
- dev
SubDomain:
Description: Sub Domain
Type: String
Default: elb
AllowedPattern: ^[^.]*$
ResourceName:
Description: Resource Name
Type: String
Default: viaelb
EC2ImageId:
Description: >
Amazon Linux 2023 on Amazon EC2 - Amazon Linux 2023
https://docs.aws.amazon.com/ja_jp/linux/al2023/ug/ec2.html#launch-via-aws-cli
https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64/description
e.g. al2023-ami-2023.3.20240122.0-kernel-6.1-x86_64
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
ELBListenerRulePriority:
Description: ELB Listner Rule Priority
Type: Number
Default: 21
MinValue: 1
MaxValue: 50000
UseSubnetProtected:
Description: Use Protected Subnet
Type: String
Default: true
AllowedValues:
- true
- false
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Environment Configuration"
Parameters:
- SystemName
- Environment
- SubDomain
- ResourceName
- EC2ImageId
- ELBListenerRulePriority
- UseSubnetProtected
Conditions:
ShouldUseSubnetProtected: !Equals [ !Ref UseSubnetProtected, true ]
Resources:
## IAM: Role
IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SystemName}-${Environment}-${ResourceName}-ec2-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
## To push images to ECR
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
## To use the Session Manager
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
## To get secret value from Secrets Manager
- !ImportValue iam-role-SecretsManagerGetSecretValuePolicy
## IAM: Instance Profile
IAMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Ref IAMRole
Roles:
- !Ref IAMRole
## EC2(VPC): Security Group
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${SystemName}-${Environment}-${ResourceName}-ec2-sg
GroupDescription: !Sub ${SystemName}-${Environment}-${ResourceName}-ec2-sg
VpcId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-VPC
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-${ResourceName}-ec2-sg
## EC2(VPC): Security Group Inbound Rule From ELB (HTTP)
EC2SecurityGroupIngressHttpFromLoadBalancer:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref EC2SecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-elb-EC2SecurityGroup
Description: !Sub ${SystemName}-${Environment}-alb-sg
## EC2(VPC): Security Group Inbound Rule To RDS (MySQL)
EC2SecurityGroupIngressMySQLToRDS:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-rds-EC2SecurityGroup
IpProtocol: tcp
FromPort:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-rds-DBClusterEndpointPort
ToPort:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-rds-DBClusterEndpointPort
SourceSecurityGroupId: !Ref EC2SecurityGroup
Description: !Sub ${SystemName}-${Environment}-${ResourceName}-ec2-sg
## EC2: Instance
Instance:
Type: AWS::EC2::Instance
## To retain the instance to be replaced when updating the instance
UpdateReplacePolicy: Retain
Properties:
ImageId: !Ref EC2ImageId
InstanceType: t2.micro
IamInstanceProfile: !Ref IAMInstanceProfile
NetworkInterfaces:
- DeviceIndex: "0"
Description: Primary network interface
AssociatePublicIpAddress: !If [ ShouldUseSubnetProtected, false, true ]
GroupSet:
- !Ref EC2SecurityGroup
SubnetId:
Fn::If:
- ShouldUseSubnetProtected
- Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-SubnetProtectedC
- Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-SubnetPublicC
Monitoring: false
UserData:
Fn::Base64: !Sub |
#!/bin/bash
dnf -y update
aws configure set default.region ${AWS::Region}
## Install Docker Engine
dnf install -y docker
systemctl enable --now docker
## Install Docker Compose
CLI_DIR=/usr/local/lib/docker/cli-plugins
LATEST_RELEASE=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep browser_download_url | grep -i $(uname -s)-$(uname -m) | grep -v sha256 | cut -d : -f 2,3 | tr -d \")
mkdir -p ${!CLI_DIR}
curl -sL ${!LATEST_RELEASE} -o ${!CLI_DIR}/docker-compose
chmod +x ${!CLI_DIR}/docker-compose
ln -s ${!CLI_DIR}/docker-compose /usr/bin/docker-compose
## Run Docker Container
docker container run --name nginx --restart=always -d -p 80:80 nginx
## Required in Amazon Linux 2, but no longer required in Amazon Linux 2023
# BlockDeviceMappings:
# - DeviceName: /dev/xvda
# Ebs:
# VolumeSize: 8
# VolumeType: gp3
# # Encrypted: true
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-${ResourceName}
PropagateTagsToVolumeOnCreation: true
## ELB: Target Group
ELBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${SystemName}-${Environment}-${ResourceName}-tg
TargetType: instance
Protocol: HTTP
Port: 80
VpcId:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-vpc-VPC
HealthCheckEnabled: true
HealthCheckProtocol: HTTP
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 5
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: "200,301"
# TargetGroupAttributes:
# - Key: stickiness.enabled
# Value: true
# - Key: stickiness.lb_cookie.duration_seconds
# Value: 3600
Targets:
- Id: !Ref Instance
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-${ResourceName}-tg
## ELB: Listener Rule
ELBListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-elb-ListenerHttps
Priority: !Ref ELBListenerRulePriority
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- !Sub
- "${SubDomain}.${DomainName}"
- DomainName:
Fn::ImportValue: !Sub ${SystemName}-${Environment}-route53-HostedZoneDomainName
- Field: path-pattern
PathPatternConfig:
Values:
- /*
Actions:
- Type: forward
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref ELBTargetGroup
Outputs:
## IAM: Role
IAMRole:
Value: !Ref IAMRole
Export:
Name: !Sub ${AWS::StackName}-IAMRole
IAMRoleArn:
Value: !GetAtt IAMRole.Arn
Export:
Name: !Sub ${AWS::StackName}-IAMRoleArn
## IAM: Instance Profile
IAMInstanceProfile:
Value: !Ref IAMInstanceProfile
Export:
Name: !Sub ${AWS::StackName}-IAMInstanceProfile
IAMInstanceProfileArn:
Value: !GetAtt IAMInstanceProfile.Arn
Export:
Name: !Sub ${AWS::StackName}-IAMInstanceProfileArn
## EC2(VPC): Security Group
EC2SecurityGroup:
Value: !Ref EC2SecurityGroup
Export:
Name: !Sub ${AWS::StackName}-EC2SecurityGroup
EC2SecurityGroupVpcId:
Value: !GetAtt EC2SecurityGroup.VpcId
Export:
Name: !Sub ${AWS::StackName}-EC2SecurityGroupVpcId
## EC2: Instance
Instance:
Value: !Ref Instance
Export:
Name: !Sub ${AWS::StackName}-Instance
InstanceAvailabilityZone:
Value: !GetAtt Instance.AvailabilityZone
Export:
Name: !Sub ${AWS::StackName}-InstanceAvailabilityZone
InstancePrivateDnsName:
Value: !GetAtt Instance.PrivateDnsName
Export:
Name: !Sub ${AWS::StackName}-InstancePrivateDnsName
InstancePrivateIp:
Value: !GetAtt Instance.PrivateIp
Export:
Name: !Sub ${AWS::StackName}-InstancePrivateIp
## ELB: Target Group
ELBTargetGroup:
Value: !Ref ELBTargetGroup
Export:
Name: !Sub ${AWS::StackName}-ELBTargetGroup
ELBTargetGroupLoadBalancerArns1:
Value: !Select [ 0, !GetAtt ELBTargetGroup.LoadBalancerArns ]
Export:
Name: !Sub ${AWS::StackName}-ELBTargetGroupLoadBalancerArns1
ELBTargetGroupFullName:
Value: !GetAtt ELBTargetGroup.TargetGroupFullName
Export:
Name: !Sub ${AWS::StackName}-ELBTargetGroupFullName
ELBTargetGroupName:
Value: !GetAtt ELBTargetGroup.TargetGroupName
Export:
Name: !Sub ${AWS::StackName}-ELBTargetGroupName
## ELB: Listener Rule
ELBListenerRule:
Value: !Ref ELBListenerRule
Export:
Name: !Sub ${AWS::StackName}-ELBListenerRule
【変更履歴】
- 2022-04-04:EBS VolumeTypeを
gp2
からgp3
に変更 - 2022-05-23:EC2ImageIdをAmazon Linux 2のKernel 4.14から5.10に変更、EBS Volumeから
DeleteOnTermination: true
とEncrypted: false
を削除 - 2022-06-03:UserDataのコマンドを
docker run
からdocker container run
に変更 - 2022-06-04:docker-composeのバージョンを
1.27.4
から2.6.0
に変更 - 2023-04-05:MetaDataに
ELBListenerRulePriority
を追加(追加漏れ) - 2023-04-06:冒頭でImportValueについてのコメント補足追加
- 2023-04-23:AMIのParameter変更、UserDataでdocker-composeのバージョンを自動で最新版になるように変更
- 2023-10-30:
PropagateTagsToVolumeOnCreation: true
を追加 - 2024-01-26:EC2インスタンスのOSをAmazon Linux 2からAmazon Linux 2023に変更
5. 予習
- 解説動画の通り、設定・作成してください ※CFnでエラーになった場合は、EventsタブのスクリーンショットをSlackでください (AWSの画面が変わっている場合は同じ設定項目を探してください)
- CFnで作成したELB関連リソースを全て、手でも作ってください。作成中に分からなかった設定項目については調べたり、質問のためにメモしておいてください ※CFn記載順も参考にして頂きたいですが、手で作る場合はALB(Load Balancer)+Listner+Target Groupがセットになります。EC2は先にあった方が手間は減りますが、後からでも問題ないです
- 【宿題】下記の項目について調べて、次回のZoomで説明してください (公式の情報が優先で、分からない場合はブログ等を参照ください)
- ALBの設定と挙動を理解するために、解説手順の動作確認でアクセスした8回のリクエストについて、各応答結果を上記の表にまとめてください。①HTTPリクエストはAWSの中でどう通って、②最終的に誰(=Server)が、③どんなHTTP応答(=HTTPステータスコード)を返したか? ④それらの設定箇所はAWSコンソール画面でどこにあるか? を調べてください。分かりづらいかもしれないので、一部だけ答えを書いておきました。
※EC2を既に作成してしまった場合は、EC2のCFnスタックを削除すると戻せます。動作確認後にお手数ですが、再度EC2のCFnスタックを作成し直して頂ければと思います
※4行目と8行目でHTTPステータスコードが分からない場合、curlコマンドで
-k
オプションが漏れている可能性が高いです。3.解説手順のコマンドを再度よくご覧の上、-k
オプションを付けて再度実行してみてください - ALBリスナールールで指定できる「条件(Condition)」「アクション(Action)」の種類とその意味
- ALBの料金体系。1ヶ月連続で動かした場合はいくらかかるか?(計算上、1ヶ月=30日×24時間とする)
- ELBセキュリティポリシー(SslPolicy) は何種類あるか? どんな違いがあるか? 具体的にはTLSバージョン(TLS Protocols) と 暗号スイート(TLS Ciphers) に注目ください。 ※ちなみにTLSバージョン、暗号スイート、それぞれ対応するブラウザの詳細は Transport Layer Security - Wikipedia に記載がありますので、詳しく知りたい方はご参考まで
- (HTTPの基礎知識として)HTTP/1.1 と HTTP/2 の違い。後者について「h2」とは何か?
- (HTTPの基礎知識として)HTTPステータスコード 200、301、302、304、403、404、405、460(ALB固有)、500、502、503、504 の意味
- ELBターゲットグループでターゲットのEC2が「Healthy」であることを確認する。さらに「Healthy」以外のステータスは何があるか?
- ELBターゲットグループでスティッキーセッションを有効にするとどうなるか?
- 今回ELB配下のEC2インスタンスではEIPが不要になったが、代わりにPublic IP Addressの自動割当が必要な場合がある。また不要な場合もある。それぞれどういう場合か? それぞれの理由も含めてお答えください
- SSL Labs の SSL Server Test (https://www.ssllabs.com/ssltest/) で公開したELBのドメイン(elb.[ご自身のドメイン名])を入力してSSLの安全性をチェックする。Do not show the results on the boards に必ずチェック☑ を入れること。入れないとドメインが公開されてしまうので
- Submitボタンを押した後に表示されるServerを確認する。これらはRoute 53に登録したAレコードを名前解決したものだが、何のリソースのIPv4アドレスか? 何個あるか? → 確認後、どれか適当に1つをクリックする
- Alternative namesにSAN (Subject Alternative Name)が表示されているか?(*.[ご自身のドメイン名]を登録したはず)
- DNS CAAが有効になっているか?
- TLSのバージョンは何が有効か?
- 暗号スイート (Cipher Suites) に赤色はないか? ※安全性が高いと緑色、低いとオレンジ色、危険性があると赤色になる
- 対応するOSやブラウザを確認する(# Not simulated clients の [Click here to expand] ボタンも押してください)
- 全体を通して赤色の危険性がないことを確認する(オレンジ色はない方が良いが、仕方ない場合もある)
- 【応用課題】余裕のある方は実際に挑戦して頂くと勉強になると思います! ※時間の都合でZoomで解説はしませんので、答え合わせをされたい方はSlackでDMください
- (実際の運用ではやりませんが、リスナールールの勉強として)curlコマンドでアクセスした場合のみ、curlのマニュアルページ (https://man7.org/linux/man-pages/man1/curl.1.html) に302リダイレクトされるルールを追加して、動作確認する ※curlコマンド以外のブラウザ(ChromeやFirefox)でアクセスするとリダイレクトされないことも確認してください
- ALBターゲットグループに追加したターゲットがAとBの2つあるとして、
- 全てのターゲット(AとB)がHealthyだった場合の挙動は当然両方にリクエストを振り分けます。では、
- 1つのターゲット(A)だけがHealthyだった場合の挙動は?
- 全てのターゲット(AとB)がUnhealthyだった場合の挙動は?(実際に動作確認する場合は、webコンテナは起動したままで、ヘルスチェックの設定ミスでUnhealthyになった場合を想定してください)
- 今回Route 53に追加したAレコードは「エイリアスレコード」と呼ばれるものだが、CNAMEレコードを使う場合(=CNAMEレコードでALBドメイン名に名前解決後、ALBドメイン名からIPアドレスに名前解決する場合)と比較して良い点は何か? また「エイリアスレコード」の名前解決(クエリ)の料金はいくらか?
- Public SubnetにEC2インスタンスを配置する場合、⑤EC2ではEIP (Elastic IP Address) を使ったが、今回はPublic IP Addressの自動割当を使っている。なぜ今回はEIPを使わないのか?
## EC2作成前の動作確認 1つ目の結果
$ curl -v http://elb.[ご自身のドメイン名]/
(中略)
< HTTP/1.1 301 Moved Permanently
< Server: awselb/2.0
< Date: Thu, 12 Dec 2024 15:07:28 GMT
< Content-Type: text/html
< Content-Length: 134
< Connection: keep-alive
< Location: https://elb.[ご自身のドメイン名]:443/
段階 | URL | HTTP応答 | Server | 経路 | 設定箇所 |
---|---|---|---|---|---|
EC2作成前 |
| 301 ※上記(中略)の下 | awselb/2.0 ※上記(中略)の下 | (リクエストがServerにたどり着くまでの流れは?) | (左記の経路にするための設定はどこに?) |
(同上) |
| ||||
(同上) |
| ||||
(同上) |
| ||||
EC2作成後 |
| ||||
(同上) |
| ||||
(同上) |
| ||||
(同上) |
|
6. FAQ(随時追加)
↓この箇所ですね。
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true
私も正直良く分からず、とりあえず全部ブロックすれば安全ということで避けてました(笑)。調べたところ、大変分かりやすい記事がありましたのでご覧ください → S3のブロックパブリックアクセスが怖くなくなった【AWS S3】
host-header
とpath-pattern
とquery-string
が分かりません。URLの構造を理解して頂くと分かると思います。それぞれ下記のHost
とPath
とQuery string
のことです。それ以外の要素もこの機会に覚えておきましょう。
ドメイン購入後、下記のようなメールが届きますので、必ずURLをクリックして有効性を認証してください。もし有効期限が切れた場合は、バリュードメインまでお問合せください。
From: <verification-noreply@onamae.com> ※value-domain.comではないのでご注意 Subject: 【重要】[バリュードメイン] ドメイン 情報認証のお願い (中略) ────────────────────────────────── ■メールアドレスの有効性認証■ ────────────────────────────────── 本メールはICANNのWhois情報正確性確認方針に基づき、レジストラより ドメイン名の登録者(Registrant)にご登録いただいているメールアドレスへ 送信しております。 ドメイン登録者情報のメールアドレスとして情報が正しい場合は、期日までに 以下URLへアクセスしてください。 対応期日:20**年**月**日 **:** https://www.onamae.com/domain/verification?authId=(以下略) ドメイン情報認証のお手続きが期限内に行われない場合、該当ドメイン を利用したホームページの閲覧や、メールの送受信ができなくなります ので、必ずお手続きをお願いいたします。 ※メールアドレス以外にも、Whois情報が不正確なドメインは、登録抹消や 使用停止の対象となることがございますのでご注意ください。 ※各種情報の確認・修正等はご利用のドメイン管理会社へご相談くださいます ようお願いいたします。
余談ですが「ICANNとは何か?」「Whoisとは何か?」も調べてみると勉強になると思います。
Linuxで最も良く使われる圧縮形式でWindowsでは標準で対応していませんので、複数形式の圧縮・解凍に対応する 7-Zip https://sevenzip.osdn.jp/ をご利用ください。
実際の現場では両方必要です。目的に合わせて使い分けています。
(1) ALBのログ
クライアント(ブラウザ)からのリクエストを直接受け付け、ALB単体で301/302/403/404などのレスポンスを返す場合もログに残ります。そのためクライアントとの通信(リクエスト・レスポンス)のログを見たい場合は、ALBのログを見る必要があります。こちらはインフラの担当者が見ることが多いです。
(2) アプリケーションのログ
それに対してアプリケーション側でバグやエラーが発生した場合は、アプリケーションのログの方が向いています。ユーザーを識別するIDやCookieの情報などをログに出力していれば、ユーザーの操作を後追いできるはずです。こちらはバックエンドの担当者が見ることが多いです。
以上、問題が起きた場合はインフラ側の問題なのか・アプリケーション側の問題なのか切り分けるのも含め、インフラエンジニア(クラウドエンジニア)とバックエンドエンジニアが協力して対処していきます。
Amazon Athena(アテナ)を使うと簡単に検索できます。余談ですが、アテナはギリシャの首都アテネと同じ語源で、ギリシャ神話に出てくる知恵の女神の名前です。参考:Youは何のサービス?AWSの面白サービス名と認定試験の話 | DevelopersIO
Athenaは、内部的にはApache Hadoop(ハドゥープ)というビッグデータを活用するための分散処理技術を使っているようです。S3バケット内の多数のログファイルを集約し、あたかもDBのテーブルのように見せて、クエリを実行できます。使ってみると感動します。
使い方の詳細は公式ドキュメント(Application Load Balancer ログのクエリ - Amazon Athena)を確認頂ければと思います。ただテーブル作成時の最初の行(テーブル名)と最後の行(読み込むS3バケットのパス)で、オススメも書いておきます。
-- テーブルの作成 (CREATE TABLE)
CREATE EXTERNAL TABLE IF NOT EXISTS alb_logs (
-- (中略)
-- 読み込むS3バケットのパス (LOCATION)
LOCATION 's3://your-alb-logs-directory/AWSLogs/<ACCOUNT-ID>/elasticloadbalancing/<REGION>/'
のままでは(最後の行で)S3バケット内の全ログを集約してしまうので使いづらいです。まず
CREATE DATABASE alb_logs;
で、データベースを作ると多数のテーブルをグルーピングして扱いやすくなります。その上で
-- alb_logs → alb_logs.awsmaster_prod_alb_202201
CREATE EXTERNAL TABLE IF NOT EXISTS alb_logs.awsmaster_prod_alb_202201 (
-- (中略)
-- 2022/01/ を末尾に追加
LOCATION 's3://your-alb-logs-directory/AWSLogs/<ACCOUNT-ID>/elasticloadbalancing/<REGION>/2022/01/'
のように(最初の行で)データベース内にテーブルを作成します。また「年(上記2022
)」「月(上記01
)」もしくは「年」を(最初の行)テーブル名と(最後の行)S3バケットのパスに追加することをオススメします。そうすると大量のデータを一度に読み込まずに済みますので。
そのほかALBのログとAthenaには注意点が多数ありますので、まとめておきます。
※ALBのログの日時はUTC(日本時間-9時間)なので時差にご注意ください。たいていUTCのことが多いです。
※ALBログの各項目の詳細については、公式ドキュメント Application Load Balancer のアクセスログ - Elastic Load Balancing に記載があります。知らない間に項目が増えていることがあり、その場合はAthenaのテーブル作成クエリもあわせて修正した方が良いです(変えなくても使えますが)。また公式ドキュメントは日本語版の更新が遅く、英語版との差異があることも多いのでご注意ください。
※上記ドキュメントに「Elastic Load Balancingはベストエフォートベースでリクエストを記録します。アクセスログは、すべてのリクエストを完全に報告するためのものではなく、リクエストの本質を把握するものとして使用することをお勧めします」と記載されています。一部のログが抜け落ちる場合もあることは、頭の片隅に置いておいてください。
※もちろんALBログ以外でもAthenaは使えます。AWS公式のサービスは AWS サービスログをクエリする - Amazon Athena を参照ください。それ以外ApacheやNginxなどのアプリケーションでもテーブルが作成できれば、自由に使えます。分からない場合はググってみてください。
※他のAWSサービスと違い、Athenaでは名前(テーブル名や項目名)に「ハイフン-
」が使えません。代わりに「アンダースコア_
」を使用してください。
以上、お疲れさまでした!