Amazon ECSをInternal ALB公開する基本構成のCloudformation

ECSにデプロイしたWEBサービスを内部ALBで公開するためのベースとなるCloudformationを作りました。

構成概要

実際は要件次第で色々付け加えると思うので、ここではあまり多くは詰め込まずシンプルな構成としてます。

f:id:yomon8:20210209082459p:plain

パラメータ

パラメータ 説明 設定例
ClusterName クラスタ名 MyCluster
ServiceName クラスタで起動するサービス名 my-service
Vpc ECSを配置するVPC vpc-1234abcd
ALBSubnetIds ALBが利用するサブネットのリスト subnet-1234abcd,subnet-wxyz6789
ALBSecurityGroupId ALBのセキュリティグループ sg-1234abcd
ServiceSubnets ECSサービスの利用するサブネットのリスト subnet-1234abcd,subnet-wxyz6789
ServiceSecurityGroupId ECSサービスのセキュリティグループ sg-1234abcd

クラスター、サービスなどの概念は以下がわかりやすいです。

ソースコード

以下がソースコードとなります。最低限必要な設定以外はほとんどそぎ落としています。必要に応じて調整して使ってください。

---
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS cluster Template

Parameters:
  ClusterName:
    Type: String
  ServiceName:
    Type: String
  AppName:
    Type: String
  VpcId:
    Type: String
  ALBSubnetIds:
    Type: List<String>
  ALBSecurityGroupId:
    Type: String
  ServiceSubnets:
    Type: List<String>
  ServiceSecurityGroupId:
    Type: String
  AppContainerPort:
    Type: Number
    Default: 80

Resources: 
#---------------------------------
# Internal ALB
#---------------------------------
  InternalALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: !Sub "${ClusterName}-alb-internal"
      Tags:
        - Key: Name
          Value: !Sub "${ClusterName}-alb-internal"
      Scheme: "internal"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
        - Key: "access_logs.s3.enabled"
          Value: false
      SecurityGroups:
        - !Ref ALBSecurityGroupId
      Subnets: !Ref ALBSubnetIds

  InternalALBTargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      VpcId: !Ref VpcId
      Name: !Sub "${ServiceName}-group"
      Protocol: HTTP
      Port: 80
      TargetType: ip

  InternalALBListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref InternalALBTargetGroup
          Type: forward
      LoadBalancerArn: !Ref InternalALB
      Port: 80
      Protocol: HTTP

#---------------------------------
# ECS Role 
#---------------------------------
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ClusterName}-ECSTaskExecutionRole"
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  ECSTaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ClusterName}-ECSTaskRole"
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole

#---------------------------------
# ECS Cluster 
#---------------------------------
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref ClusterName 
      CapacityProviders:
        - FARGATE
        - FARGATE_SPOT
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Base: 1
          Weight: 1
        - CapacityProvider: FARGATE_SPOT
          Weight: 2
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

#---------------------------------
# ECS Task 
#---------------------------------
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${ClusterName}"

  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: nginx
      Cpu: 256
      Memory: 512
      NetworkMode: awsvpc
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      TaskRoleArn: !Ref ECSTaskRole
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: !Ref AppName
          Image: nginx
          Essential: true
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ClusterName
              mode: non-blocking
              max-buffer-size: 100m
          PortMappings:
            - ContainerPort: !Ref AppContainerPort
              HostPort: 80

#---------------------------------
# ECS Service 
#---------------------------------
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      DesiredCount: 1
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups: 
            - !Ref ServiceSecurityGroupId
          Subnets: !Ref ServiceSubnets
      Cluster: !Ref ECSCluster
      ServiceName: !Ref ServiceName
      TaskDefinition: !Ref ECSTaskDefinition
      LoadBalancers:
        - ContainerName: !Ref AppName
          ContainerPort: !Ref AppContainerPort
          TargetGroupArn: !Ref InternalALBTargetGroup
    DependsOn:
      - InternalALBListener

Outputs:
  Endpoint:
    Value: !Sub "http://${InternalALB.DNSName}"

使い方

Cloudformationでパラメータを設定してスタックをデプロイします。 f:id:yomon8:20210209084855p:plain

スタックをデプロイ後にOutputにURLが出ているので確認します。 f:id:yomon8:20210208224531p:plain

Internal ALBなのでVPC内からcurl等で確認してみます。

$ curl -s http://internal-mycluster-alb-internal-123456789012.ap-northeast-1.elb.amazonaws.com/ | head
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }

Cfn補足説明

ALB

設定的にはECSから参照されるだけです。本当に必要な設定くらいしかしていないです。

#---------------------------------
# Internal ALB
#---------------------------------
  InternalALB:
#--省略

ECS Role

#---------------------------------
# ECS Role 
#---------------------------------
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
#--省略
  ECSTaskRole:
    Type: AWS::IAM::Role
#--省略

タスクロールとタスク実行ロールの二つを定義しています。それぞれがどこに当たるかは以下の図がわかりやすいです。

ECS Cluster

#---------------------------------
# ECS Cluster 
#---------------------------------
  ECSCluster:
    Type: AWS::ECS::Cluster
#--省略

Clusterは境界や箱のようなもので実体は別途タスク定義として設定します。

ここでポイントとなるのは containerInsights が有効化されていることと、DefaultCapacityProviderStrategy の部分です。

FARGATEとFARGATE_SPOTを設定していますが、この設定の意味はこちらの記事を参照するとわかりやすいと思います。

dev.classmethod.jp

ECS Task

#---------------------------------
# ECS Task 
#---------------------------------
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${ClusterName}"

  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
#--省略

ここもnginxのコンテナ設定しているだけで他は特殊な設定抜くようにしています。

1点だけ、ログの出力先としてCloudWatch Logsを指定するため、awslogsの設定を行っています。設定項目は以下に記載があります。

docs.aws.amazon.com

さいごに

最近EKSをずっと使っていたのですが、その後でECS使うと余計にシンプルに感じますね。