APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

【CloudFormation】ALBとPrivateLink を使用してWeb アプリケーションをインターネットに公開する

目次

はじめに

こんにちは。クラウド事業部の西川です。

今回は下記のブログを参考にALBとPrivateLinkを使用してWeb アプリケーションをインターネットに公開する方法をご紹介します。

aws.amazon.com

AWS PrivateLinkやVPCエンドポイントに関しては下記のブログが非常にわかりやすいです。 また、コンソールでのPrivateLink作成方法も詳しく解説されています。

dev.classmethod.jp

VPC同士の接続といえば、VPCピアリングの使用が真っ先に思い浮かぶのではないかと思いますが、今回のようなVPC同士を接続してWebアプリケーションを公開するシナリオでは下記のようなPrivateLinkを使用するメリットがあります。

まず注目すべきなのは異なるVPC間でのIPアドレスの範囲が重複していても接続が可能である点です。基本的にVPCピアリングではIPアドレスの範囲が重複している場合、ピアリングの設定はできませんが、PrivateLinkではこの制約がありません。

また、PrivateLinkは指定されたサービスのみを他のVPCに公開します。例えば、ポート80(HTTP)でリッスンするWebアプリケーションの場合、PrivateLinkを介したElastic Network Interface(ENI)はHTTP接続のみを受け入れます。これにより、Application-VPCに対して不要なトラフィックが入ることを防ぎ、セキュリティを向上させることができます。

さらに、PrivateLinkを使用した場合、接続はDMZ-VPCからApplication-VPCへの一方向のみで開始できます。つまり、Application-VPCからDMZ-VPCへの接続の開始はできず、不正アクセスのリスクを減少させ、より管理しやすいネットワーク環境を実現することができます。

構成図

DMZ-VPCに配置されたALBをリバースプロキシとして使用し、 PrivateLinkで接続されたインターネットゲートウェイの無いApplication-VPCのアプリケーションサーバー(今回はApache Webサーバー)を公開します。

参考ブログと異なる点としてVPCは2つだけ作成し、ALBのパスベースルーティングも省略しています。

また、この記事ではCloudFormationである程度リソースを作成し、実際に手を動かして作成するのはALBとALB用のターゲットグループのみです。

事前準備(AMI作成)

まず最初にApplication-TemplateのEC2の起動時に指定するAMIを作成します。今回はApacheをインストール&テストページを作成したものを用意します。

下記のテンプレートをCloudFormationで使用してスタックを作成してください。

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create a VPC, an Internet Gateway, a Public Subnet, and an EC2 Instance.

Parameters:
  PJPrefix:
    Type: String
  EC2AMIId:
    Description: AMI ID
    Type : String
    Default: ami-0d0150aa305b7226d
  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-AMICreation-VPC"

  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: MyInternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVPC
      InternetGatewayId: !Ref MyInternetGateway

  MyPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-AMICreation-PublicSubnet"

  MyRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-AMICreation-RouteTable"

  MyRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref MyRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref MyInternetGateway

  MySubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref MyPublicSubnet
      RouteTableId: !Ref MyRouteTable

  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow SSH and HTTP
      VpcId: !Ref MyVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-AMICreation-SecurityGroup"

  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t2.micro
      SecurityGroupIds:
        - !Ref MySecurityGroup
      SubnetId: !Ref MyPublicSubnet
      KeyName: !Ref KeyName
      ImageId: !Ref EC2AMIId
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-AMICreation-EC2Instance"
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum -y update
          yum -y install httpd
          systemctl start httpd
          systemctl enable httpd
          echo "<html>
          <head>
          <title>Test Page</title>
          </head>
          <body>
          <h1>Test Page</h1>
          <p>If you see this page, the web server is working correctly.</p>
          </body>
          </html>" > /var/www/html/index.html

スタック作成後、AMIを作成してください。AMI作成後はスタックを削除していただいて問題ありません。

AMIの作成方法は下記のブログで丁寧に紹介されています。

dev.classmethod.jp

CloudFormationを初めて触る方は下記の記事のスタック作成の項目を参考にスタックを作成してください(RDSの設定などは不要です)

techblog.ap-com.co.jp

テンプレート

Application-Template

AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFormation template for Application-VPC

Parameters:
  PJPrefix:
    Type: String
    Default: "PrivateLink-HandsOn"
  VPCCIDR:
    Type: String
    Default: "192.168.0.0/20"
  PublicSubnetCIDR:
    Type: String
    Default: "192.168.0.0/24"
  PrivateSubnetCIDR:
    Type: String
    Default: "192.168.1.0/24"
  EC2AMIId:
    Description: AMI ID
    Type : String
  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"

Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-Application-VPC"

  PrivateSubnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnetCIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-Application-PrivateSubnet"

  PrivateRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-Application-PrivateRoute"

  PrivateSubnetRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupDescription: "Allow access to EC2 instances"
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 80
          ToPort: 80
          CidrIp: "0.0.0.0/0"

  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      InstanceType: "t2.micro"
      SubnetId: !Ref PrivateSubnet
      KeyName: !Ref KeyName
      SecurityGroupIds:
        - !Ref SecurityGroup
      ImageId: !Ref EC2AMIId
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-Application-WebServerA"

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VPC
      Name: !Sub "${PJPrefix}-NLB-TG"
      Protocol: TCP
      Port: 80
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-NLB-TG"
      Targets:
        - Id: !Ref EC2Instance
          Port: 80

  NLB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Subnets:
        - !Ref PrivateSubnet
      Name: !Sub "${PJPrefix}-NLB"
      Scheme: "internal"
      Type: "network"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: "false"
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-NLB"

  NLBListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - Type: "forward"
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref NLB
      Port: 80
      Protocol: "TCP"

  VPCEndpointService:
    Type: AWS::EC2::VPCEndpointService
    Properties:
      AcceptanceRequired: false
      NetworkLoadBalancerArns:
        - !Ref NLB

Outputs:
  VPCEndpointService:
    Value: { "Fn::Sub": "com.amazonaws.vpce.${AWS::Region}.${VPCEndpointService}" }
    Export:
      Name: !Sub "${PJPrefix}-VPCEndpointService"

DMZ-Template

AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFormation template for DMZ-VPC

Parameters:
  PJPrefix:
    Type: String
    Default: "PrivateLink-HandsOn"
  VPCCIDR:
    Type: String
    Default: "192.168.0.0/20"
  PublicSubnetACIDR:
    Type: String
    Default: "192.168.1.0/24"
  PublicSubnetCCIDR:
    Type: String
    Default: "192.168.2.0/24"
  PrivateSubnetCIDR:
    Type: String
    Default: "192.168.3.0/24"

Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: "true"
      EnableDnsHostnames: "true"
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-DMZ-VPC"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-VPC-IGW
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-VPC-RouteTable
  Route:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PublicSubnetACIDR
      MapPublicIpOnLaunch: 'true'
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-VPC-PublicSubnet-A
  PublicRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetC:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PublicSubnetCCIDR
      MapPublicIpOnLaunch: 'true'
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${PJPrefix}-VPC-PublicSubnet-C
  PublicRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable

  PrivateSubnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PrivateSubnetCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-DMZ-PrivateSubnet"

  PrivateRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-DMZ-PrivateRoute"

  PrivateSubnetRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  EndPointSG:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "http"
      GroupName: !Sub "${PJPrefix}-DMZ-SG"
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-DMZ-SG"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort : 80
          ToPort : 80
          CidrIp: 0.0.0.0/0

  VpcEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcId: !Ref VPC
      ServiceName: { "Fn::ImportValue": !Sub "${PJPrefix}-VPCEndpointService" }
      VpcEndpointType: "Interface"
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref EndPointSG

リソース作成

AMIが作成できたら、上記のテンプレートを使用してリソースを作成します。

スタックはApplication-Templateスタック→DMZ-Templateスタックの順番で作成してください。 Application-Templateスタック作成時に事前準備で作成したAMIのIDを指定します。

※注:パラメーターのPJPrefixは同一のものにしてください。

続いて、ALBとALBのターゲットグループをコンソールで作成します。

まずはコンソールのVPC>仮想プライベートクラウド>エンドポイントのサブネットの項目でエンドポイントのIPアドレスを確認します。今回の自分の環境では192.168.3.145でした。

そしてEC2>ロードバランシング>ロードバランサーに移動し、右上の「ロードバランサーの作成」をクリックします。

作成画面でApplication Load Balancerの「作成」をクリックしてください。

基本的な設定の項目のロードバランサー名は適当なものを付けてください。スキームはインターネット向け、IP アドレスタイプはIPv4のままで問題ありません。

ネットワークマッピング の項目のVPCではDMZ-VPCを選択し、マッピングは2つのAZのパブリックサブネットを選択してください。

続いてセキュリティグループではDMZ-SGを選択してください。

その次のリスナーとルーティングの項目にある「ターゲットグループの作成」をクリックするとターゲットグループの作成画面に遷移します。

基本的な設定のターゲットタイプの選択は「IPアドレス」を選択し、ターゲットグループ名に適当な名前を付けてください。

その他の項目はデフォルトで問題ありませんが、念のためVPCの項目ではDMZ-VPCが選択されていることを確認してください。

「次へ」を押すとターゲットを登録する画面が表示されます。

ここで先ほど確認したエンドポイントのIPアドレスを入力します。

IPアドレスの入力後、「保留中として以下を含める」をクリックし、「ターゲットグループの作成」を押してください。

ターゲットグループが作成できたら先ほどのALBの作成画面に戻ります。

リスナーとルーティングのデフォルトアクションの右にあるリロードボタンを押すと、先ほど作成したターゲットグループが表示されるのでこれを選択します

設定に誤りがなければ「ロードバランサーの作成」をクリックしてください。

ALBがアクティブになるまで少し待ちます。

ALBがアクティブになったのち、ターゲットグループを確認してください。

下記のように登録済みターゲットのヘルスステータスがHealthyになっていればOKです。 ※下記画像では環境を作り直したためIPアドレスが変わっています。

動作確認

ではEC2のApache Webサーバーにインターネットからアクセスできるか、動作確認をします。

EC2>ロードバランシング>ロードバランサーに移動し、ALBのチェックボックスをクリックしてDNS名をコピーします。(下記画像の画面右下)

コピーしたDNS名でブラウザからアクセスし、下記画面が表示されれば動作確認はOKです。

リソース削除

ALB (+ターゲットグループ) → DMZ-Templateスタック → Application-Templateスタックの順番で削除してください。 AMI作成用のスタックとAMIの削除もお忘れなく。

まとめ

ALBとPrivateLinkを使用してWeb アプリケーションをインターネットに公開する方法をご紹介しました。

ご参考になれば幸いです。