7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CloudFormation による ECS 環境の構築

Last updated at Posted at 2021-08-16

想定読者

  • aws の基礎知識がある
  • docker の基礎知識がある

前準備

① aws cli のインストール

② IAM ユーザの作成

CloudFormation でリソースを構築できる IAM ユーザーの作成。以下の点に注意して作成します。

  • CloudFormation を実行できる権限があるか(admin 権限がついていれば作成可能)
  • プログラムによりアクセスを許可する
  • IAM ユーザー発行時に発行される、アクセスキー IDシークレットアクセスキーをメモしておく

IAMユーザーの作成では下記のサイトが参考になります。

必要に応じて IAM ユーザーの権限を絞ってください。一般的に最低限の権限を与えることが良いとされています。

IAM ポリシーを作成する場合、最小限のアクセス権を付与するという標準的なセキュリティアドバイスに従うか、タスクの実行に必要なアクセス許可のみ付与します。

出典:https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/best-practices.html#grant-least-privilege

③ 上記で作成したユーザーの アクセスキー IDシークレットアクセスキー を aws cli の credentials に設定

以下を ~/.aws/credentials に追記

~/.aws/credentials
[cloudformation_sample]
aws_access_key_id=${アクセスキー ID}
aws_secret_access_key=${シークレットアクセスキー}

以下を ~/.aws/config に追記

~/.aws/config
[profile cloudformation_sample]
region=ap-northeast-1

④ docker の install

⑤ Dockerfile の作成

本記事では以下の Dockerfile を使います。あまり意味のない Dockerfile ですが、build して image を作成したいのでこのようにしています。

Dockerfie
FROM nginx

⑥ Route53 でドメインの取得(オプション)

なくても大丈夫です。CloudFormation を使った Route53 の設定もしたい方は必要です。

概要

CloudFormation を使って下図で示す構成を構築します。

システム構成図.png

ECR に登録された docker image をもとに、Fargate を使ってコンテナを起動します。コンテナへのアクセスはロードバランサを使って制御します。コンテナのログは CloudWatch へ、ロードバランサへのログは S3 に残します。また、コンテナの cpu 使用率が閾値を超えた際に自動でスケールするように、AutoScaling を設定していきます。

CloudFormation で作るメリット

簡単に同じ環境を構築、削除できることです。

例えば、検証用のテスト環境の構築を任されたとします。もちろん、AWS上のコンソールでポチポチして作ることも可能ですが、本記事の用に多くのリソースを作成する場合は大変です。また、人間なので必ずミスも起きます。作成のみならず削除も大変です。料金がかかるサービスもあるので、削除が必要ですが、削除漏れや、誤って必要なリソースを削除してしまう可能性があります。

一方で、CloudFormation を使ってコードでインフラを定義していれば、コマンド一つで検証環境の構築・削除ができます。工数も削減できますし、100%再現できます。削除漏れもありません。

システム構築方法

  • 下記ファイルをカレントディレクトリに用意する

    以下のようになっていればOK

    ls
    Dockerfile parameters.ecr.json parameters.ecs.json template.ecr.yml template.ecs.yml
    
  • ECR の作成

    ❯ aws cloudformation create-stack \
    --profile cloudformation_sample \
    --stack-name cloudformation-sample-ecr \
    --capabilities CAPABILITY_IAM \
    --template-body file://$(pwd)/template.ecr.yml \
    --parameters file://$(pwd)/parameters.ecr.json
    
    ## output
    {
        "StackId": "arn:aws:cloudformation:ap-northeast-1:${account_id}:stack/cloudformation-sample-ecr/${id}"
    }
    

    account_id は後で使うのでメモしておく

  • ECR に docker image を登録

    ❯ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com
    ❯ docker build -t cloudformation-sample-ecr .
    ❯ docker tag cloudformation-sample-ecr ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/cloudformation-sample-ecr:latest
    ❯ docker push ${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/cloudformation-sample-ecr:latest
    
  • ECS の作成

    ❯ aws cloudformation create-stack \
    --profile cloudformation_sample \
    --stack-name cloudformation-sample-ecs \
    --capabilities CAPABILITY_IAM \
    --template-body file://$(pwd)/template.ecs.yml \
    --parameters file://$(pwd)/parameters.ecs.json
    
  • 確認

    ALB の DNS 名にアクセスして nginx の画面が映れば OK

alb console.png

nginx http.png

パラメーターの作成

以下のような理由でパラメーターを別ファイルで定義しています。
パラメーターをテンプレートファイルから分離していると、再利用性が高まる。例えば、パラメータファイルをコピーして vpc や subnet の id を書き換えるだけで全く同じ環境を dev と prod に簡単に構築できる。
また、github 等にソースコードをアップロードすることを考えても template に id などをハードコーディングしていると誰からでも id が見えてセキュリテイィー的によろしくないと思われます。

parameters.ecr.json

parameters.ecr.json
[
  {
    "ParameterKey": "ProjectName",
    "ParameterValue": "cloudformation_sample"
  }
]

parameters.ecs.json

  • ${VPC ID}, ${Private Subnet IDs}, ${Public Subnet IDs} はご自身のものに書き換えてください
  • ALB の CIDR が 0.0.0.0/0 とフルオープンになっているので必要に応じて変更してください
parameters.ecs.json
[
  {
    "ParameterKey": "ProjectName",
    "ParameterValue": "cloudformation_sample"
  },
  {
    "ParameterKey": "VpcId",
    "ParameterValue": "${VPC ID}"
  },
  {
    "ParameterKey": "PrivateSubnetIds",
    "ParameterValue": "${Private Subnet IDs}" // ","区切りで書く
  },
  {
    "ParameterKey": "PublicSubnetIds", // ","区切りで書く
    "ParameterValue": "${Public Subnet IDs}"
  },
  {
    "ParameterKey": "ALBAllowCidrIp",
    "ParameterValue": "0.0.0.0/0"
  },
  {
    "ParameterKey": "ECSDesiredCapacity",
    "ParameterValue": "1"
  },
  {
    "ParameterKey": "ECSMaxCapacity",
    "ParameterValue": "2"
  },
  {
    "ParameterKey": "ECSContainerPort",
    "ParameterValue": "80"
  },
  {
    "ParameterKey": "ECSContainerLogsRetentionInDays",
    "ParameterValue": "30"
  },
  {
    "ParameterKey": "ECSTaskCpuMB",
    "ParameterValue": "512"
  },
  {
    "ParameterKey": "ECSTaskMemory",
    "ParameterValue": "2048"
  },
  {
    "ParameterKey": "TargetGroupHealthCheckPath",
    "ParameterValue": "/"
  }
]

テンプレートの作成

template.ecr.yml

template.ecr.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Create ECR

Parameters:
  ProjectName:
    Type: String

Resources:
  ECR:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub ${ProjectName}-ecr

Outputs:
  ECR:
    Value: !GetAtt ECR.RepositoryUri
    Export:
      Name: !Sub ${ProjectName}-ecr-repository-uri

template.ecs.yml

template.ecs.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  ProjectName:
    Type: String
    Description: Project name.
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Select a VPC that allows instances access to the Internet.
  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Select at public subnets in your selected VPC.
  PublicSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Select at public subnets in your selected VPC.
  ALBAllowCidrIp:
    Type: String
    Description: CIDR IP of VPN.
  ECSDesiredCapacity:
    Type: Number
    Description: Number of instances to launch in your ECS cluster.
  ECSMaxCapacity:
    Type: Number
    Description: Maximum number of instances that can be launched in your ECS cluster.
  ECSContainerPort:
    Type: Number
    Description: Port Number of container created by the task.
  ECSContainerLogsRetentionInDays:
    Type: Number
    Description: Number of days to retain the log events of container created by the task.
  ECSTaskCpuMB:
    Type: Number
    Description: Number of cpu units used by the task.
  ECSTaskMemory:
    Type: Number
    Description: Amount of memory used by the task.
  TargetGroupHealthCheckPath:
    Type: String
    Description: Destination for health checks on the targets.
Mappings:
  S3Config:
    us-east-1:
      BucketPrincipal: '127311923021'
    us-east-2:
      BucketPrincipal: '033677994240'
    us-west-1:
      BucketPrincipal: '027434742980'
    us-west-2:
      BucketPrincipal: '797873946194'
    ca-central-1:
      BucketPrincipal: '985666609251'
    eu-west-1:
      BucketPrincipal: '156460612806'
    eu-central-1:
      BucketPrincipal: '054676820928'
    eu-west-2:
      BucketPrincipal: '652711504416'
    ap-northeast-1:
      BucketPrincipal: '582318560864'
    ap-northeast-2:
      BucketPrincipal: '600734575887'
    ap-southeast-1:
      BucketPrincipal: '114774131450'
    ap-southeast-2:
      BucketPrincipal: '783225319266'
    ap-south-1:
      BucketPrincipal: '718504428378'
    sa-east-1:
      BucketPrincipal: '507241528517'
    us-gov-west-1:
      BucketPrincipal: '048591011584'
    cn-north-1:
      BucketPrincipal: '638102146993'

Resources:
  ###################################################
  # IAM
  ###################################################
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ecsTaskExecutionPolicy
          PolicyDocument:
            Statement:
              Effect: Allow
              Action:
                - ecr:GetAuthorizationToken
                - ecr:BatchCheckLayerAvailability
                - ecr:GetDownloadUrlForLayer
                - ecr:BatchGetImage
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: '*'

  AutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - application-autoscaling.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: ecsAutoScalingPolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: 
                  - application-autoscaling:*
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:PutMetricAlarm
                  - ecs:DescribeServices
                  - ecs:UpdateService
                Resource: '*'


  ###################################################
  # SecurityGroup
  ###################################################
  ECSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn: ALBSecurityGroup
    Properties:
      GroupDescription: ECS Security Group
      VpcId: !Ref VpcId
  ECSSecurityGroupHTTPinbound:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: !Sub ${ProjectName}-alb
      GroupId: !Ref ECSSecurityGroup
      IpProtocol: tcp
      FromPort: !Ref ECSContainerPort
      ToPort: !Ref ECSContainerPort
      SourceSecurityGroupId: !Ref ALBSecurityGroup

  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ECS Security Group
      VpcId: !Ref VpcId
  ALBSecurityGroupHTTPinbound:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: vpn ip
      GroupId: !Ref ALBSecurityGroup
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      CidrIp: !Ref ALBAllowCidrIp


  ###################################################
  # ECS
  ###################################################
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${ProjectName}-cluster
  ECSService:
    Type: AWS::ECS::Service
    DependsOn:
      - ALBHTTPListener
    Properties:
      ServiceName: !Sub ${ProjectName}-service
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSDesiredCapacity
      LaunchType: FARGATE
      LoadBalancers:
        - ContainerName: !Ref ProjectName
          ContainerPort: !Ref ECSContainerPort
          TargetGroupArn: !Ref TargetGroup
      TaskDefinition: !Ref ECSTaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ECSSecurityGroup
          Subnets: !Ref PrivateSubnetIds
  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${ProjectName}
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: !Ref ProjectName
          PortMappings:
            - ContainerPort: !Ref ECSContainerPort
              HostPort: !Ref ECSContainerPort
              Protocol: tcp
          Image:
            Fn::ImportValue:
              !Sub ${ProjectName}-ecr-repository-uri
      Cpu: !Ref ECSTaskCpuMB
      Memory: !Ref ECSTaskMemory
      TaskRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      RequiresCompatibilities: 
        - FARGATE
      NetworkMode: awsvpc


  ###################################################
  # ALB
  ###################################################
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${ProjectName}-alb
      Scheme: internet-facing
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: '30'
        - Key: access_logs.s3.enabled
          Value: 'true'
        - Key: access_logs.s3.bucket
          Value: !Sub ${ProjectName}-alb-logs
      Subnets: !Ref PublicSubnetIds
      SecurityGroups:
        - !Ref ALBSecurityGroup
  ALBHTTPListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref ALB
  ALBHTTPListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      Conditions:
        - Field: path-pattern
          Values:
            - '*'
      ListenerArn: !Ref ALBHTTPListener
      Priority: 1
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: ALB
    Properties:
      HealthCheckIntervalSeconds: 60
      HealthCheckPath: !Ref TargetGroupHealthCheckPath
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      Name: !Sub ${ProjectName}-tg
      Port: 80
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      TargetType: ip
      VpcId: !Ref VpcId


  ###################################################
  # AutoScale
  ###################################################
  AutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MaxCapacity: !Ref ECSMaxCapacity
      MinCapacity: 1
      ResourceId:
        Fn::Join:
          - ''
          - - service/
            - !Ref ECSCluster
            - /
            - !GetAtt ECSService.Name
      RoleARN: !GetAtt AutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
  AutoScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub ${ProjectName}-target-tracking-policy
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref AutoScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        PredefinedMetricSpecification: 
          PredefinedMetricType: ECSServiceAverageCPUUtilization 
        ScaleInCooldown: 300
        ScaleOutCooldown: 300
        TargetValue: 70


  ###################################################
  # CloudWatch
  ###################################################
  CloudwatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /ecs/${ProjectName}
      RetentionInDays: !Ref ECSContainerLogsRetentionInDays


  ###################################################
  # S3
  ###################################################
  ALBLogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ProjectName}-alb-logs
  BucketPolicyELBLogBucket:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref ALBLogBucket
      PolicyDocument:
        Id: !Sub ${ProjectName}-alb-logs-policy
        Version: '2012-10-17'
        Statement:
          - Action:
              - s3:PutObject
            Effect: Allow
            Resource:
              Fn::Join:
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref ALBLogBucket
                  - /AWSLogs/
                  - !Ref AWS::AccountId
                  - /*
            Principal:
              AWS:
                Fn::FindInMap:
                  - S3Config
                  - !Ref AWS::Region
                  - BucketPrincipal

Route53 の設定

設定を追記

  • ${ssl 証明書の arn}, ${Domain の hostzone ID}, ${Domain} はご自身のものに書き換えてください

    parameters.ecs.json
    [
      // 追記
      {
        "ParameterKey": "ACMArn",
        "ParameterValue": "${ssl 証明書の arn}"
      },
      {
        "ParameterKey": "HostZoneId",
        "ParameterValue": "${Domain の hostzone ID}"
      },
      {
        "ParameterKey": "Domain",
        "ParameterValue": "${Domain}"
      }
    ]
    
    template.ecs.yml
    Parameters:
    
      (中略)
      
      ACMArn:
        Type: String
        Description: Select Arn for ssl/tls certificate.
      HostZoneId:
        Type: AWS::Route53::HostedZone::Id
        Description: ID of the hosted zone.
      Domain:
        Type: String
        Description: Name of domain. 
     
      (中略)
    
     Resources:
     
     	(中略)
     	
      ECSService:
          Type: AWS::ECS::Service
          DependsOn:
            - ALBHTTPListener
            - ALBHTTPSListener //追記
            
      (中略)
      
      ALBSecurityGroupHTTPSinbound:
        Type: AWS::EC2::SecurityGroupIngress
        Properties:
          Description: vpn ip
          GroupId: !Ref ALBSecurityGroup
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref ALBAllowCidrIp
    
      ALBHTTPSListener:
        Type: AWS::ElasticLoadBalancingV2::Listener
        Properties:
          Port: 443
          Protocol: HTTPS
          Certificates:
            - CertificateArn: !Ref ACMArn
          DefaultActions:
            - TargetGroupArn: !Ref TargetGroup
              Type: forward
          LoadBalancerArn: !Ref ALB
      ALBHTTPSListenerRule:
        Type: AWS::ElasticLoadBalancingV2::ListenerRule
        Properties:
          Actions:
            - Type: forward
              TargetGroupArn: !Ref TargetGroup
          Conditions:
            - Field: path-pattern
              Values:
                - '*'
          ListenerArn: !Ref ALBHTTPSListener
          Priority: 1
    
      DNSRecordSet:
        Type: AWS::Route53::RecordSetGroup
        Properties:
          HostedZoneId: !Ref HostZoneId
          RecordSets:
            - Name: !Ref Domain
              Type: A
              AliasTarget:
                HostedZoneId: !GetAtt ALB.CanonicalHostedZoneID
                DNSName:
                  Fn::Join:
                    - '.'
                    - - dualstack
                      - !GetAtt ALB.DNSName
    

Stack をアップデート

❯ aws cloudformation update-stack \
--profile cloudformation_sample \
--stack-name cloudformation-sample-ecs \
--capabilities CAPABILITY_IAM \
--template-body file://$(pwd)/template.ecs.yml \
--parameters file://$(pwd)/parameters.ecs.json

確認

指定したドメインにアクセスすると、nginx の画面が表示されればOK

nginx https.png

参考

7
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?