初めてでしかもCloudFormationでAWS環境構築(EC2&ALB)

初めてでしかもCloudFormationで環境構築(EC2&ALB) AWS
スポンサーリンク

初めてAWSで環境構築することになって、それもCloudFormation(cfnと略すようです)でリソースを立てる必要のある方なんてそうそういないかもしれませんが、私がそのような境遇になったので、同じような方が迷わないよう、私の備忘録も兼ねて記しておきます。

ここまで丁寧に解説してくれているサイトは見つからなかったので。

 



インフラ構築の前提条件

1. 私の当時の経験

  • Rails勉強して、Railsチュートリアル完走済、ポートフォリオはつくれる、程度のレベル
  • CLFは取得済(どんな資格か気になる方はこちらを参照してみてください

これだけです。CloudFormationの存在自体はCLF取得の過程で知っていました。あれでしょ?コードでインフラ環境を定義できる、いわゆるIaC(Infrastructure as Code)ってやつでしょう?くらいの知識でしたがww
なのでこの記事でも、各サービスの説明は省いています。(ALBって何?みたいな説明)

2. 今回構築するAWS環境

今回は、VPCやIGW, VPNは既にあるという前提条件。

  • パブリックサブネットをつくり、EC2を1台立ち上げ(踏み台サーバー)
  • プライベートサブネットをつくり、EC2を1台立ち上げ(Webサーバー)
  • Webサーバーからインターネットにアクセスできるよう、NATゲートウェイを設置
  • 今回事情があり、WebサーバーにはVPNからHTTP(Port:8080)でアクセス。それをセキュアにする(HTTPSにする)ために、ロードバランサーを用意して、ACMで取得したSSL/TLS証明書を持たせる。(SSLオフロード機能

はい、大きく、この4つだけです。ACMだけはCLF勉強時にも出てこなかったので未知でしたが、他はまあ、よくある、踏み台サーバーとWebサーバーの組み合わせです(当時の私が「踏み台サーバー」でググったことは内緒です)

が、ルートテーブル設定とか、EIP取得とか、DNS登録とか、色々な付帯作業があることを、その時の私は知らなかったんですね・・・本当に素人丸出しでしたが、具体的にどうやったかこの後、解説しますのでご安心ください!

 



初めてでもCloudFormation(cfn)でリソースを構築する手順

1. まずやること

触ったことない方は、せめてAWSの公式ハンズオンはやっておくと良いです。
申込みフォームの記入がやや面倒ですが、無料ですし、2~3時間くらいで1周できますよ(動画自体はもっと短いです)
これで雰囲気を掴めますし、基本形となるかなり単純な形のテンプレートの雛形がダウンロードできます。本記事もそうですが、ネット上にもテンプレートのサンプルってたくさん転がっているのですが、とにかく長くて、初めて見る人にとっては「おえ」ってなるので、短いものを理解して、それから自分でどんどん付け足していくのがオススメです。その点、上記ハンズオンで配布されている雛形は短いけれどもそれだけでリソースを構築できるので、雛形にするにはもってこいでした。

2. 進め方

上記ハンズオンの中でも紹介されていますが、テンプレートリファレンスがCloudFormationの辞書のような役割です。基本的にこのテンプレートリファレンスを見ながら進めていきます。現在英語版しかないみたいなので、chromeの機能で日本語訳してもらって見ていきました。

しかし、ほとんど初めてAWSを触る場合、リソースを立てるのにそもそもどんな設定が必要なのか?わかりませんよね。どれが必須の設定か、一応、リファレンスにも書いてあるのですが、

  • 「条件付き必須」というのがあり、タイプによっては必須になる場合がある。
  • 必須ではないけれども、指定しないと勝手にタイプが選ばれるため、実質的に必須の場合がある。

みたいなパターンもあるため、初めてなのにリファレンスだけ見て読み解いていくのは賢くありません。

そこで、実際にマネジメントコンソールから手動でリソースを立ち上げてみることをオススメします!正確には、立ち上げる一歩手前まで設定してみる、ということです。マネジメントコンソールから手動でリソースを立ち上げる場合、

  1. そのリソースのトップページにいき、「作成する」や「起動する」といったオレンジのボタンを押す。
  2. 設定をする。(この画面が複数ある場合もある)
  3. 「次へ」ボタンを押すと、設定の確認画面が現れる。
  4. 確認画面で、「作成する」や「起動する」といったオレンジのボタンを押す。

この順に立ち上げることがほとんどで、4で最後のオレンジのボタンを押さない限り、リソースが作られることはありません。途中でいつでもキャンセルできますし、少なくとも、「次へ」ボタンはいくら押しても大丈夫です。(なので、実際に立ち上げるわけではないので既存の環境に変更を加えることはありません。)

そうやって設定していくと、「これは必須ではないけど、選択肢として何があるから明示的に書いておくか」などの判断ができるようになります。

このように、

  • テンプレートリファレンスを確認
  • マネジメントコンソールで実際に手動でリソースの設定をしてみる

これを繰り返すことで、CloudFormationのテンプレートを作成していくのが最もオーソドックスかつ確実だと思います。

作ってみてダメなら作り直せば良い、という考え方もあるかと思いますが、ここを変更するには作り直さないといけない(一度削除しなくてはならない)こともあるので、少し慎重になっても良いかと思います。私はGitと連携してダブルチェックしてもらいました。品質の担保が難しいんですよね。ちなみに、構文エラーなどは、ちゃんとリソース作成前に事前にエラーを返してくれるので大丈夫です。

また、テンプレートの記述形式はJSONとYAMLを選べるのですが、圧倒的にYAML形式がオススメです。見やすいですし、閉じ括弧の入力ミスでエラーなんていうこともありませんから編集も容易です。組み込み関数の短縮形が使えるのも大きく、メリットばかりです。
万が一、JSON必須の場合も、自動変換ツールがありますので、それを利用されることをオススメします。

テンプレート解説(ブロック毎)

それでは、早速、ブロックごとに見ていきましょう!

0. パラメータの設定

最初の1行はフォーマットが決まっていますので、このとおりに書きます。しかも、2021年3月現在、”2010-09-09″のみが有効な値なので注意です。勝手にstack作成日とかに変更してはいけません。

繰り返し使う or 変わり得るものについては、パラメータで与えておくと良いです。

今回、Outputsの設定は行っていません。なくても一応リソースは出来ます。

AWSTemplateFormatVersion: 2010-09-09 # この日付は"2010-09-09"で固定(2021年3月現在)
Description: Comments # Stackに関するコメントが書ける。ただし、全角は文字化けするので注意。

Parameters: # 何度も出てくる or 変わり得るものについては、パラメーターで与えておくと良い。
  sysname: # プロジェクトタグなどにシステムの名前をつけておけば、後からそのシステムに関するリソースをピックアップできる。
    Type: String
    Default: System's-Name
  VPCID:
    Type: String
    Default: vpc's-id # 既存のVPCのIDをコピペ
  AZname1:
    Type: String
    Default: ap-northeast-1a # 既存のAZの名前をコピペ
  AZname2:
    Type: String
    Default: ap-northeast-1c # 既存のAZの名前をコピペ
  ACMArnToELBin:
    Type: String
    Default: arn:aws:acm:ap-northeast-1:123456789... # ELBに持たせるACMのARNをコピペ

1. サブネットの作成(AWS::EC2::Subnet)

既存のVPCにサブネットを作成します。!Refとかは組み込み関数と呼ばれるものです。

Resources:
  # Create subnets in the existing VPC
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.0/26 # プライベートIPアドレスの範囲を指定
      VpcId: !Ref VPCID # VPCを指定
      AvailabilityZone: !Ref AZname1 # AZを指定
      MapPublicIpOnLaunch: true # パブリックサブネットではtrueにする。
      Tags: # Tagはあってもなくても問題ないし、後からでもつけられる。
        - Key: Name
          Value: !Sub ${sysname}-publicsubnet01
        - Key: Project
          Value: !Ref sysname
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.64/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname1
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatesubnet01
        - Key: Project
          Value: !Ref sysname
  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.128/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname2
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-publicsubnet02
        - Key: Project
          Value: !Ref sysname
  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.192/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname2
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatesubnet02
        - Key: Project
          Value: !Ref sysname

2. NATゲートウェイの作成(AWS::EC2::NatGateway)

プライベートサブネットに設置するWebサーバーからインターネットにアクセスできるよう、NATゲートウェイを設置します。NATゲートウェイにはEIPが必要なので、EIPも同時に生成します。EIPのIDを取得するには!GetAttを使ってください。!RefだとIPアドレスが与えられます。


  # Create NAT gateway with a new Elastic IP address
  EIP01: # NATゲートウェイにはEIPが必要。
    Type: AWS::EC2::EIP # とりあえずEIPを作成(取得)する
    Properties: 
      Domain: vpc # これは書かなくてもきっと大丈夫だが一応明記
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-eip01
        - Key: Project
          Value: !Ref sysname
  NAT01:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId: !GetAtt EIP01.AllocationId # ここで、EIPのIDを指定
      SubnetId: !Ref PublicSubnet01 # NATゲートウェイを設置するパブリックサブネットを指定
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-nat01
        - Key: Project
          Value: !Ref sysname

3. ルートテーブルの作成(初心者要注意!)

さて、囲い(=サブネット)を立てて、門(ゲートウェイ)を設置したのでネットワーク周りはこれでOKかというと、全く、そんなことはありません。

まだ道(=ルート)がありません!

道がなければ、だれも敷地から出られないことになります。(正確には、何も設定しないとメインルートテーブルが関連付けられる。デフォルトのままだとローカルルートのみ。VPCの敷地内だったらアクセスできるよってやつ。これから設定するのはメインルートテーブルに対してカスタムルートテーブルと呼ばれているが、この記事では簡単のため単にルートテーブルと呼ぶこととする。)

ルートが必要なのはわかった、じゃあどう設定するか?これが素人にとっては少々複雑だったので、丁寧に解説していきます。

まず、トラフィックを許可する(アクセスできるようにする)ためには、「誰ならどこからどこへ行ってよい」という情報を特定の表に記載する、すなわち

  • A ルートテーブル (= A行き先一覧表、地図と言ってもよい)に
  • B 誰が (= IPアドレスなどの条件)
  • C どこから (= 出発地、具体的にはサブネットのこと)
  • D どこへ (= 行き先、具体的にはゲートウェイやインスタンスなど)

通ることを許可するかを指定する必要がある。(A~Dを設定して、かつ、関連付ける必要がある)

これを踏まえた上で、次の3つの作業(ブロック)でルートを設定することになります。

作業イメージA~Dのどれに当たるかCloudformationにおけるResource type
1ルートテーブル(の枠組みの作成)行き先一覧表の枠だけつくっておくイメージ。VPCだけ指定。まずAだけ作成AWS::EC2::RouteTable
2ルート(の作成)ルートテーブルに「誰はどこへ行っていいよ」情報を書くイメージ。Aに、BとDの情報を追記AWS::EC2::Route
3ルートテーブルをサブネットに関連付け誰は「どこから」どこへ行っていいよ、の「どこから」を指定する。AとCを関連付けAWS::EC2::
SubnetRouteTableAssociation
  1. まず、予めVPCを指定して、何も書かれていない空白のルートテーブル(A)を作成する。
  2. 次に、ルートテーブルに、「誰はどこへ行っていいよ」情報を追記する。これがルート。ルートはAとBとDの情報(誰がどこへ行っていいかをこのルートテーブルに書くよ!な情報)を持つ。ルートは複数追記可能。
  3. 最後に、「このルートテーブルはどこから行く場合の情報なのか」を追記する。これがルートテーブルをサブネットへ関連付ける、ということ。AとCの情報を持つ。1つのサブネットに対して、必ず1つのルートテーブルを関連付ける必要がある。

これで、関連付けられたサブネット(C)の中のリソースと、外部のDとのルートが開通して、アクセスできるようになります。(ちなみに、!Refを使っている限り、順番は入れ替え可能です。心配ならDependsOnを使うと良いでしょう。)

特に、ルートの作成に関しては、「書かれているルートテーブル」を指定することのみ必須になっているんですが、「誰が」「どこへ」行ってよいのか、すなわち「通って良い人の条件」と「行き先」の情報を持たないとルートを作成する意味がないので、実質、必須であるというのは押さえておく必要があります。(それらを指定する方法が色々あるため、「必須ではない」になっているだけ)

以上より、コードを見ると理解しやすいと思います。コメントで補足してあります。


  # Create RouteTable for Public-Subnet
  PublicRT: # VPCだけ指定してルートテーブルを作成
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPCID
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-publicrt
        - Key: Project
          Value: !Ref sysname
  PublicRouteToIGW: # ルートテーブルを指定して、トラフィックの行き先と条件を指定
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref publicRT # ルートテーブルを指定(必須)
      DestinationCidrBlock: 0.0.0.0/0 # IPアドレス=通ってよい人の条件(実質必須)
      GatewayId: igw-0123456789 # InternetゲートウェイのID=行き先の情報(実質必須)
  PublicSubnet01Association: # ルートテーブルの関連付け(ルートテーブルを指定して、トラフィックの出発点を指定)
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet01 # 指定したルートテーブルを関連付けるサブネットを指定
      RouteTableId: !Ref publicRT # ルートテーブルを指定

  # Create RouteTable for Private-Subnet
  PrivateRT: # 今回はそれぞれのサブネットに対して、異なるルートテーブルを作成。
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPCID
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatert
        - Key: Project
          Value: !Ref sysname
  PrivateRouteToNAT:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref privateRT
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NAT01
  PrivateSubnet01Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet01
      RouteTableId: !Ref privateRT

  # Enable route transmission to VPN Gateway
  VPNGatewayRoute: # これで「ルート伝播」がtrueになり、なんとすべてのVPNとのトラフィックを許可できる。面倒なRoute設定が不要!
    Type: AWS::EC2::VPNGatewayRoutePropagation
    Properties: 
      RouteTableIds: 
        - !Ref publicRT
        - !Ref privateRT
      VpnGatewayId: vpn-0123456789 # 既存のVPNゲートウェイのID

最初、定期券や空港の時刻表みたいに、出発地→到着地のリストみたいなのを想像してしまったのが誤解の元でした。しかし、これで無事ネットワーク周りは整備できました!(今回はVPC, IGW, VPNは既存のものを活用する形だったため、多少イージーモードだったかもしれません)

4. 踏み台/Webサーバー用のEC2インスタンスの作成(AWS::EC2::Instance)

やっと土地の整地(=サブネットやゲートウェイの設置)と道路整備(=ルートの設定)が終わったので、土地に家(=サーバー)を立てていくことが出来ます。

EC2は、色々設定があるので、マネジメントコンソールのハンズオンが大切ですね。要件定義書があるなら、そこに記載されているスペックを満たすよう設定できれば大丈夫です。

まず、EC2にはEBSをアタッチしますね。それには、EC2のオプションでAWS::EC2::Instance BlockDeviceMappingを使用します。ここ、最初わかりにくかったので。

また、当たり前らしいのですが、家には施錠(=セキュリティ対策)しないといけません。

今回は、下記2つで施錠します。

そして、キーペアは、秘密鍵をダウンロードする必要があるため、キーペアはCloudFormationでは作れません!ここ、注意してください。したがって、マネジメントコンソールで予め作っておく必要があります!

セキュリティーグループは、ホワイトリストです。すなわち、「誰々は入っていいよ」のリストです。ルートテーブルでいくら道が整備されていても、ここで許可されていないと結局アクセスできないので重要です。(デフォルトでは誰も入れない)

踏み台サーバーのパブリックIPアドレスが変動すると厄介なので、EIPも作成します。EIPは作成したらリソースに割り当てないといけないのですが、NATゲートウェイに割り当てる際はNATゲートウェイ作成時、EC2インスタンスに割り当てる際はEIP作成時に割り当てるので注意です(ややこしい)。既存の、割り当てされていないEIPがあるなら、AWS::EC2::EIPAssociationを用いて後から割り当てることもできます。(割り当てされていないEIPがあると課金されてしまうので、あまりないとは思いますが・・・リソースのお引越しとかだとありそうですね。)

あとの細かい注意事項はコード中のコメントを御覧下さい。

# ------------------------------------------------------------#
#  Server and SecurityGroup
# ------------------------------------------------------------#
  # Create EC2 and SecurityGroup for Bastion-server
  BastionSV: # 踏み台サーバーを作成
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-09d28faae2e9e7138 # AMIのIDを指定
      InstanceType: t3.micro # タイプを指定
      SubnetId: !Ref PublicSubnet01 # どこのサブネットにつくるか指定。踏み台サーバーはパブリックに配置。
      KeyName: Key-Pair-Name # アクセスするための、予め作成しておいたキーペア名を指定=公開鍵を置ける
      SecurityGroupIds: # セキュリティグループを指定。
        - !Ref BastionSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-Bastionsv
        - Key: Project
          Value: !Ref sysname
  BastionSG: # セキュリティグループ(外から内部にアクセス可能なホワイトリスト)を作成。
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for bastion server # なくてよい
      GroupName: !Sub ${sysname}-bastionsg # なくてよい
      VpcId: !Ref VPCID # どこのVPCにつくるか指定
      SecurityGroupIngress: # インバウンドルールの設定。既にあるSGに追加する場合は、AWS::EC2::SecurityGroupIngressを使用すれば良い。
        - IpProtocol: tcp # VPNから踏み台サーバーへSSH接続を許可
          CidrIp: 172.XXX.XXX.0/23
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp # 自分のパソコンから踏み台サーバーへSSH接続を許可
          CidrIp: 122.XXX.XXX.XXX/32 # 自分のパソコンのようなある特定のIPアドレスからのアクセスを許可する場合でも、/32が必要。
          FromPort: 22
          ToPort: 22
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-bastionsg
        - Key: Project
          Value: !Ref sysname
  EIP02: # 踏み台サーバーのパブリックIPアドレスが変動すると厄介なので、EIPを作成する。
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      InstanceId: !Ref BastionSV # インスタンスに割り当てる場合は、ここで指定する。
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-eip02
        - Key: Project
          Value: !Ref sysname

  # Create EC2 and SecurityGroup for Web-server
  WebSV: # webサーバーを作成
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-059b6d3840b03d6dd
      InstanceType: m5.xlarge
      SubnetId: !Ref PrivateSubnet01 # プライベートサブネットに作成
      KeyName: Key-Pair-Name # 踏み台サーバーと同一のキーペアを指定
      BlockDeviceMappings: # ルートボリュームの仕様を指定できる。(デフォルトで良ければ特に指定不要)
      - DeviceName: "/dev/sda1" # ルートボリュームの仕様を指定するなら必須。デフォルトで良ければ、実際にマネジメントコンソールで立ち上げてみて確認すること。
        Ebs:
          VolumeSize: 100 # 単位はGiB
          VolumeType: gp2 # デフォルトがgp2だが、明示的に指定。
      SecurityGroupIds:
        - !Ref WebSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-websv
        - Key: Project
          Value: !Ref sysname
  WebSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for web server # なくてもよい
      GroupName: !Sub ${sysname}-websg # なくてもよい
      VpcId: !Ref VPCID
      SecurityGroupIngress:
        - IpProtocol: tcp # VPNからwebサーバーへSSH接続を許可
          CidrIp: 172.XXX.XXX.0/23
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp # 踏み台サーバからwebサーバーへSSH接続を許可
          SourceSecurityGroupId: !Ref BastionSG
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp # さらにELBからのHTTP接続を許可。プライベートIPアドレスは変わるため、SGで指定する。
          SourceSecurityGroupId: !Ref ELBSG
          FromPort: 8080 # 今回は8080を指定(おそらくなんでも良い)。
          ToPort: 8080
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-websg
        - Key: Project
          Value: !Ref sysname

ec2は立ち上げたら、「初期設定」が必要。最低限
・OSの最新化
・JSTタイムゾーン変更
の2点だけはやっておくのが常識のようです。(本当は他にも色々あるのだが、その色々を言い出すときりがないしケースバイケースなことも多いので、「『いつか使うかも』は使わないと思え」の考えに則って、最低限とした)

CloudFormationでも、最初立ち上げ時、UserDataプロパティを使えば、EC2のプロビジョニングで実行できるシェルスクリプトが書けるので、普通は合わせて記載するようです。私は知らなかった今回は諸事情あり上記には書いていません。(”UserData”にシェルスクリプトが書ける、なんて初見じゃわからんわ!)
参考:https://qiita.com/algi_nao/items/8898afed7ce723ea7fbb

Amazon Linux 2のEC2:

$ sudo yum update -y

ダウンロードにこれくらい容量食うけどよろしい?みたいな確認があるだけだったので、オプション-yつけてしまってよいと思います。
参考:https://www.atmarkit.co.jp/ait/articles/1608/29/news019.html

Ubuntu 20.04 LTSのEC2:

$ sudo apt-get update
$ sudo apt-get upgrade

参考:https://qiita.com/kotarella1110/items/f638822d64a43824dfa4

JSTタイムゾーンの変更:https://hacknote.jp/archives/52688/

ちなみに、今回は踏み台サーバーを設置しているため、Webサーバーへ入るには多段SSH接続が必要です。やり方はこちら

5. SSLオフロードを目的としたELB(とACM)の作成(AWS::ElasticLoadBalancingV2::LoadBalancer)

最後の難関、やってきました。ロードバランサーは、元々は負荷分散によってパフォーマンスを向上したり可用性を上げるために用いるものですが、今回はHTTP接続しか出来ないwebアプリへセキュアな接続を実現するために(具体的には、ACMで作成したSSL/TLS証明書を持たせるために)、ELBの一種であるALBを使用します。(SSLオフロード機能

しかも、今回、DNSサーバーはAWSのサービスであるRoute53ではなく、他社のDNSサーバーを使用するため、少々、複雑で、下記の条件を満たす必要がありました。

  • HTTPSのリスナーをつくるには、ALBと、発行済みのACMが必要。
  • ACMを「発行済み」にするには、DNSサーバーにドメインを登録しておかないといけない。
  • DNSサーバーにドメイン登録をするには、「検証待ち」のACMと、ALBを作成しておく必要あり。

ここで、「リスナー」という言葉にピンとこなかった方は、下記記事で知識補完できます。

DNSサーバーへのドメイン登録はACMもALBも同時にしたいので、以下の順序で進めていきました。

  1. AWSマネジメントコンソールで、ACMの証明書を手動作成(Pending validation「検証待ち」になる)
  2. CloudformationでALBの作成を行う。リスナーの作成はしない。なので、証明書とリスナーの関連付けもしない。(書いてコメントアウトしておいてもよい。)
  3. DNSサーバーに、ACM、ALBのドメイン登録(それぞれ、登録が必要)
  4. ACMが「発行済み」になっていることを確認
  5. CloudFormation更新でALBにHTTPSのリスナーを追加

これでうまくいきます。

# ------------------------------------------------------------#
#  Elastic Load Balancer (Application Load Balancer)
# ------------------------------------------------------------#
  # Create ELB(ALB) ELBは、デフォルトでALBが選ばれます。
  ELBinalb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${sysname}-ELBinalb
      Scheme: internal # これは必須ではないが、閉域網のロードバランサーにするなら指定必須(指定しないと、デフォルトのinternet facingになってしまう上、変更するにはELBの作り直しが必要=リソースIDなどが変わってしまうので注意)
      Subnets: # 2つ以上のAZに属するそれぞれ1つ以上のサブネットを指定する必要がある。
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroups: 
        - !Ref ELBSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-ELBinalb
        - Key: Project
          Value: !Ref sysname
  ELBinalbTargetGroup: # ELBがアクセスする負荷分散先をターゲットと呼ぶ。
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${sysname}-ELBinalbtargetgroup
      VpcId: !Ref VPCID
      Port: 8080 # 今回は8080を指定(おそらくなんでも良い)。
      Protocol: HTTP # ターゲットへアクセスするプロトコル
      Targets: # ターゲットは複数選択できる。(今回は1つ)
        - Id: !Ref WebSV
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-ELBinalbtargetgroup
        - Key: Project
          Value: !Ref sysname
  ELBSG: # ALBにもセキュリティーグループが必要。
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for ELB
      GroupName: !Sub ${sysname}-elbsg
      VpcId: !Ref VPCID
      SecurityGroupIngress:
        - IpProtocol: tcp # VPNからELBへのHTTPS接続を許可
          CidrIp: 0.0.0.0/0 # アドレス帯が不明だったため全部を指定している。
          FromPort: 443
          ToPort: 443
        - IpProtocol: tcp # 踏み台サーバからELBへのHTTPS接続を許可
          SourceSecurityGroupId: !Ref BastionSG
          FromPort: 443
          ToPort: 443
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-elbsg
        - Key: Project
          Value: !Ref sysname

  # Attach SSL Certificate for ALB ・・・ACMが「発行済み」になるまではコメントアウトしておくこと。
  ELBinalbListener: # ELBへアクセスする発信元をリスナーと呼ぶ。ELBとターゲットを紐付ける役割も果たす。
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ELBinalb # ELBを指定
      Port: 443
      Protocol: HTTPS # ELBへアクセスするプロトコル
      Certificates: # HTTPSを選択する場合には証明書が必要。
        - CertificateArn: !Ref ACMArnToELBin # ACMが「発行済み」になったら貼り付け。
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref ELBinalbTargetGroup # ターゲットを指定
  ELBinalbListenerCertificate: # ACMで作成した証明書をELB(のリスナー)に持たせる。
    Type: AWS::ElasticLoadBalancingV2::ListenerCertificate
    Properties: 
      Certificates: 
        - CertificateArn: !Ref ACMArnToELBin
      ListenerArn: !Ref ELBinalbListener

テンプレート完成! → スタック作成成功!

以上をまとめるとこんな感じになります。

AWSTemplateFormatVersion: 2010-09-09
Description: Comments

Parameters:
  sysname:
    Type: String
    Default: System's-Name
  VPCID:
    Type: String
    Default: vpc's-id
  AZname1:
    Type: String
    Default: ap-northeast-1a
  AZname2:
    Type: String
    Default: ap-northeast-1c
  ACMArnToELBin:
    Type: String
    Default: arn:aws:acm:ap-northeast-1:123456789...

Resources:
# ------------------------------------------------------------#
#  Network(Subnet/NATgateway/RouteTable)
# ------------------------------------------------------------#
  # Create subnets in the existing VPC
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.0/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname1
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-publicsubnet01
        - Key: Project
          Value: !Ref sysname
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.64/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname1
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatesubnet01
        - Key: Project
          Value: !Ref sysname
  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.128/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname2
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-publicsubnet02
        - Key: Project
          Value: !Ref sysname
  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.1.1.192/26
      VpcId: !Ref VPCID
      AvailabilityZone: !Ref AZname2
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatesubnet02
        - Key: Project
          Value: !Ref sysname

  # Create NAT gateway with a new Elastic IP address
  EIP01:
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-eip-01
        - Key: Project
          Value: !Ref sysname
  NAT01:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId: !GetAtt EIP01.AllocationId
      SubnetId: !Ref PublicSubnet01
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-nat01
        - Key: Project
          Value: !Ref sysname

  # Create RouteTable for Public-Subnet
  PublicRT:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPCID
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-publicrt
        - Key: Project
          Value: !Ref sysname
  PublicRouteToIGW:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref publicRT
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: igw-0123456789
  PublicSubnet01Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet01
      RouteTableId: !Ref publicRT

  # Create RouteTable for Private-Subnet
  PrivateRT:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPCID
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-privatert
        - Key: Project
          Value: !Ref sysname
  PrivateRouteToNAT:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref privateRT
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NAT01
  PrivateSubnet01Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet01
      RouteTableId: !Ref privateRT

  # Enable route transmission to VPN Gateway
  VPNGatewayRoute:
    Type: AWS::EC2::VPNGatewayRoutePropagation
    Properties: 
      RouteTableIds: 
        - !Ref publicRT
        - !Ref privateRT
      VpnGatewayId: vpn-0123456789

# ------------------------------------------------------------#
#  Server and SecurityGroup
# ------------------------------------------------------------#
  # Create EC2 and SecurityGroup for Bastion-server
  BastionSV:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-09d28faae2e9e7138
      InstanceType: t3.micro
      SubnetId: !Ref PublicSubnet01
      KeyName: Key-Pair-Name
      SecurityGroupIds:
        - !Ref BastionSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-Bastionsv
        - Key: Project
          Value: !Ref sysname
  BastionSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for bastion server
      GroupName: !Sub ${sysname}-bastionsg
      VpcId: !Ref VPCID
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 172.XXX.XXX.0/23
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp
          CidrIp: 122.XXX.XXX.XXX/32
          FromPort: 22
          ToPort: 22
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-bastionsg
        - Key: Project
          Value: !Ref sysname
  EIP02:
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      InstanceId: !Ref BastionSV
      Tags: 
        - Key: Name
          Value: !Sub ${sysname}-eip02
        - Key: Project
          Value: !Ref sysname

  # Create EC2 and SecurityGroup for Web-server
  WebSV:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-059b6d3840b03d6dd
      InstanceType: m5.xlarge
      SubnetId: !Ref PrivateSubnet01
      KeyName: Key-Pair-Name
      BlockDeviceMappings:
      - DeviceName: "/dev/sda1"
        Ebs:
          VolumeSize: 100
          VolumeType: gp2
      SecurityGroupIds:
        - !Ref WebSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-websv
        - Key: Project
          Value: !Ref sysname
  WebSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for web server
      GroupName: !Sub ${sysname}-websg
      VpcId: !Ref VPCID
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 172.XXX.XXX.0/23
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp
          SourceSecurityGroupId: !Ref BastionSG
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp
          SourceSecurityGroupId: !Ref ELBSG
          FromPort: 8080
          ToPort: 8080
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-websg
        - Key: Project
          Value: !Ref sysname

# ------------------------------------------------------------#
#  Elastic Load Balancer (Application Load Balancer)
# ------------------------------------------------------------#
  # Create ELB(ALB)
  ELBinalb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${sysname}-ELBinalb
      Scheme: internal 
      Subnets:
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroups: 
        - !Ref ELBSG
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-ELBinalb
        - Key: Project
          Value: !Ref sysname
  ELBinalbTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${sysname}-ELBinalbtargetgroup
      VpcId: !Ref VPCID
      Port: 8080
      Protocol: HTTP
      Targets:
        - Id: !Ref WebSV
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-ELBinalbtargetgroup
        - Key: Project
          Value: !Ref sysname
  ELBSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: sg for ELB
      GroupName: !Sub ${sysname}-elbsg
      VpcId: !Ref VPCID
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: 443
          ToPort: 443
        - IpProtocol: tcp
          SourceSecurityGroupId: !Ref BastionSG
          FromPort: 443
          ToPort: 443
      Tags:
        - Key: Name
          Value: !Sub ${sysname}-elbsg
        - Key: Project
          Value: !Ref sysname

  # Attach SSL Certificate for ALB
  ELBinalbListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ELBinalb
      Port: 443
      Protocol: HTTPS
      Certificates:
        - CertificateArn: !Ref ACMArnToELBin
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref ELBinalbTargetGroup
  ELBinalbListenerCertificate:
    Type: AWS::ElasticLoadBalancingV2::ListenerCertificate
    Properties: 
      Certificates: 
        - CertificateArn: !Ref ACMArnToELBin
      ListenerArn: !Ref ELBinalbListener

変数やIPアドレスを、きちんとした値に置き換えればスタックを作成できるはずです。

はるすと
はるすと

最後まで読んでくださってありがとうございました!

この記事を書いた人
こもれびエンジニア

自然と自由を愛するエンジニア。2021年1月に、大手製造業設計からプログラマ(Rails, AWS)へ転職。動物や自然との触れ合いや、汗を流すのが好き。

/HSP(繊細さん)/18デリケートな象/ストレングスファインダー(1分析思考/2親密性/3学習欲/4調和性/5収集心)、テニス、合気道、登山、あいだみつを、ジブリ、ワンピース、ドラゴンボール、AWS、Ruby on Rails、アイミング

twitterをフォローして、記事にならないちょっとした豆知識もチェック!
AWS
スポンサーリンク
SNSでシェア/コメントして、自分のアウトプット/発信力を高めるのにお使いください。 ↓ 各ページへジャンプ ↓
twitterをフォローして、記事にならないちょっとした豆知識もチェック!
スポンサーリンク
「そんなか」サイト

コメント

タイトルとURLをコピーしました