【AWS ECS】nginxとTomcatのコンテナをFargateで動作させる環境をCloudFormationで構築する

container

こんにちは、エンジニアの田代です。
前回記事(https://blog.tech-monex.com/entry/2020/01/31/163001)時点では証券企画室所属でしたが、今年度からマネックス・ラボ所属となりました。
やる事は変わらず、ferciの開発に携わっています。

はじめに

個人的な感覚ですが、数年前までは「本番用ではなく、あくまで開発環境のための物だ」とする意見が大多数であったように思われるDockerコンテナも、徐々に本番導入のハードルが下がってきているのではないでしょうか。
AWSのコンテナオーケストレーションサービスであるECSにおいても、サービスディスカバリがサポートされたり、Fargateのエフェメラルストレージ暗号化が標準となったりするなど、使用性が向上してきています。
そこで今回は、nginxのWebフロントとTomcatのバックエンドによって構成されるシステムを、ECS+Fargateを使用して構築するためのCloudFormationテンプレートを作成してみたいと思います。

やりたいこと

diagram

上図の通りのnginx+Tomcatの環境をFargate上に構築します。
今回はどちらも1サービスにつき1コンテナのシンプルな構成ですが、バックエンドのTomcatの前段にELBを置かず、サービスディスカバリを使用してサービス名+ドメイン名でnginxからアクセスしている部分がポイントとなります。

前提事項

当記事で扱う全ての技術的要素について解説すると膨大な文章量になってしまうので、下記を前提事項とします。

  • VPC、フロントELB用のログ転送先のS3バケット、各サブネット及びセキュリティグループが用意されていること
  • Docker及びCloudFormation(以下CFn)の操作方法に関する知識
  • nginxのconfファイルの記述内容に関する知識
  • ECS、ECR、Fargateに関する基本的な理解

ECRリポジトリとDockerコンテナの作成

まずはecr.yamlをCFnで実行して、Dockerコンテナを登録するためのECRリポジトリを作成します。
ACLの設定は省略していますが、他AWSアカウントからアクセスさせたい等の場合は追記してください。

AWSTemplateFormatVersion: "2010-09-09"

Resources:
  SampleNginxRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: sample-nginx
      Tags:
        - Key: Name
          Value: SampleNginxRepository
    
  SampleTomcatRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: sample-tomcat
      Tags:
        - Key: Name
          Value: SampleTomcatRepository

次に、nginxとTomcatの各コンテナをビルドして、ECRにプッシュします。
殆ど設定らしい設定は行なっていませんが、nginx/default.confの6行目、リバースプロキシの設定で向き先をhttp://サービス名.ドメイン名:8080/としている部分がポイントです。

FROM nginx:latest
ADD "default.conf" "/etc/nginx/conf.d/"
server {
   listen       80;
   server_name  localhost;

   location /app/ {
       proxy_pass  http://backend.sample.local:8080/;
   }

   location / {
       root   /usr/share/nginx/html;
       index  index.html index.htm;
   }

   error_page   500 502 503 504  /50x.html;
   location = /50x.html {
       root   /usr/share/nginx/html;
   }
}
FROM tomcat:latest

各ディレクトリとファイルが用意出来たら、CLIから下記のコマンドを実行してください。

$ aws ecr get-login --no-include-email --region {YOUR_REGION}
$ docker login -u AWS -p {YOUR_TOKEN} https://{YOUR_ECR_URL}.amazonaws.com
$ # -> Login Succeeded
$ docker build -t {sample-nginx|sample-tomcat} .
$ docker tag {sample-nginx|sample-tomcat}:latest {YOUR_ECR_URL}.amazonaws.com/{sample-nginx|sample-tomcat}:latest
$ docker push {YOUR_ECR_URL}.amazonaws.com/{sample-nginx|sample-tomcat}:latest

下図のように、nginxとTomcatの各リポジトリにコンテナが登録されたことがECRの画面から確認できれば成功です。

ecr

ECSリソースの作成

続いて、ECS関連のリソースを構築して行きます。
まずfront_elb.yamlをCFnで実行して、Webフロント用のinternet-facingなELBを作成します。前提事項にある通り、サブネットやセキュリティグループ等は事前に適切な設定が用意されているものとします。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  VpcId:
    Type: String
  Subnet1:
    Type: String
  Subnet2:
    Type: String
  S3Bucket:
    Type: String
  S3Prefix:
    Type: String
  ElbSecurityGroup:
    Type: String

Resources:
  FrontNginxElb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: FrontNginxElb
      Scheme: internet-facing
      Subnets:
        - !Ref Subnet1
        - !Ref Subnet2
      SecurityGroups:
        - !Ref ElbSecurityGroup 
      LoadBalancerAttributes:
        - Key: access_logs.s3.enabled
          Value: true
        - Key: access_logs.s3.bucket
          Value: !Ref S3Bucket
        - Key: access_logs.s3.prefix
          Value: !Ref S3Prefix
        - Key: "idle_timeout.timeout_seconds"
          Value: 30
      Tags:
        - Key: Name
          Value: FrontNginxElb

  FrontNginxTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher: 
        HttpCode: "200"
      Name: FrontNginxTargetGroup
      Port: 80
      Protocol: HTTP
      Tags:
        - Key: Name
          Value: FrontNginxTargetGroup
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 300
      TargetType: ip
      UnhealthyThresholdCount: 2
      VpcId: !Ref VpcId
    
  FrontNginxListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref FrontNginxTargetGroup
          Type: forward
      LoadBalancerArn: !Ref FrontNginxElb
      Port: 80
      Protocol: HTTP

最後にecs.yamlをCFnで実行して、各ECSリソースと、バックエンド用の名前空間及びサービスディスカバリを作成します。
ポイントは105行目と115行目で、これらの値をnginx/default.conf内proxy_passに設定したhttp://サービス名.ドメイン名:8080/と一致させることで、nginxからFQDNでTomcatへアクセスすることが可能となります。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  VpcId:
    Type: String
  TaskExecutionRoleArn:
    Type: String
  NginxImageId:
    Type: String
  TomcatImageId:
    Type: String
  NginxTargetGroupArn:
    Type: String
  FrontSecurityGroup:
    Type: String
  BackendSecurityGroup:
    Type: String
  FrontSubnet:
    Type: String
  BackendSubnet:
    Type: String

Resources:
  SampleEcsCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: SampleEcsCluster

  SampleEcsLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: "/ecs/logs/sample"

  SampleFrontTask:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: 256
      ExecutionRoleArn: !Ref TaskExecutionRoleArn
      Family: SampleFrontTask
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: FrontNginxContainer
          Image: !Ref NginxImageId
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref SampleEcsLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: "front"
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80

  SampleBackendTask:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: 256
      ExecutionRoleArn: !Ref TaskExecutionRoleArn
      Family: SampleBackendTask
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: BackendTomcatContainer
          Image: !Ref TomcatImageId
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref SampleEcsLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: "backend"
          PortMappings:
            - HostPort: 8080
              Protocol: tcp
              ContainerPort: 8080

  SampleFrontService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref SampleEcsCluster
      DesiredCount: 1
      LaunchType: FARGATE
      LoadBalancers:
        - TargetGroupArn: !Ref NginxTargetGroupArn
          ContainerPort: 80
          ContainerName: FrontNginxContainer
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref FrontSecurityGroup
          Subnets:
            - !Ref FrontSubnet
      ServiceName: front
      TaskDefinition: !Ref SampleFrontTask

  SampleDnsNamespace:
    Type: AWS::ServiceDiscovery::PrivateDnsNamespace
    Properties: 
      Name: sample.local
      Vpc: !Ref VpcId

  BackendServiceDiscovery:
    Type: AWS::ServiceDiscovery::Service
    Properties:
      DnsConfig:
        DnsRecords:
          - Type: A
            TTL: 60
      Name: backend
      NamespaceId: !Ref SampleDnsNamespace
    
  SampleBackendService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref SampleEcsCluster
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref BackendSecurityGroup
          Subnets:
            - !Ref BackendSubnet
      ServiceName: backend
      TaskDefinition: !Ref SampleBackendTask
      ServiceRegistries:
        - ContainerName: BackendTomcatContainer
          RegistryArn: !GetAtt BackendServiceDiscovery.Arn

実行が完了したら、Webフロント用のELBにブラウザからアクセスしてみましょう。

nginx
nginx
tomcat
Tomcat

/でnginxのデフォルト画面が、/appでTomcatのデフォルト画面が表示されれば成功です。
また、念のためRoute 53でsample.localのレコードセットを確認してみると、バックエンドのコンテナのAレコードが自動で登録されていることが分かります。

r53

まとめ

以上で、 サービスディスカバリを使用することで、バックエンド用のELBを作成することなく、タイトルの通りの環境を構築することができました。
実際にはこれらに加えてAutoScalingの設定等も必要になると思いますが、同様の構成のCFnテンプレートを作成する際の参考になれば幸いです。

過去2回インフラ寄りの内容になったので、次回は開発寄りの内容の記事が書ければと思っています。

田代 侑大システム開発部 マネックス・ラボ