🌊

19.ECS FargateをCloudFormationで動かしてみる試み

2024/08/13に公開

1.アプリケーションの作成

  • プロジェクト名をkyotoとして作成
mkdir kyoto
cd kyoto
go mod init kyoto
go get -u github.com/labstack/echo/v4
main.go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()

  e.HideBanner = true

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/status", func(c echo.Context) error {
		return c.JSON(http.StatusOK, "OK")
	})

	e.Start(":8080")
}

2.Docker イメージの作成

Dockerfile
# Step 1: Build the Go application
FROM golang:1.22.6-alpine AS builder

WORKDIR /app

COPY . .

RUN go mod tidy
RUN go build -o kyoto

# Step 2: Create a lightweight image with the compiled binary
FROM alpine:3.18.8

WORKDIR /app

COPY --from=builder /app/kyoto .

EXPOSE 8080

CMD ["./kyoto"]
# イメージのビルド
$ docker build -t kyoto . --no-cache

動作確認

  • docker run --rm -p 8080:8080 kyoto
$ curl -i localhost:8080/status
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 11 Aug 2024 00:45:56 GMT
Content-Length: 5

"OK"
$ curl -i localhost:8080/
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Sun, 11 Aug 2024 00:45:59 GMT
Content-Length: 24

{"message":"Not Found"}

ECRコスト感確認

  • ストレージ利用料にはGB/月あたり0.10USD

  • イメージサイズの確認

$ docker image ls kyoto
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
kyoto        latest    c3b398a212a6   9 minutes ago   15.5MB
  • 今回0.0152GBなので 0.0152 GB/月 x 0.10 USD = 0.0015 USD
  • 為替を150円/$として 150 x 0.0015 = 0.225円

https://aws.amazon.com/jp/ecr/pricing/

3. ECRにイメージをPush

# `aws --version`して最新でなければupgradeしておく(https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst)
$ brew upgrade awscli

# リポジトリの作成(プロジェクト名と同じ`kyoto`で作成)
$ aws ecr create-repository \
  --profile <my-profile> \
  --repository-name kyoto \
  --region <your-region> \
  --image-scanning-configuration scanOnPush=true 

# ログイン
$ aws ecr get-login-password --profile <my-profile> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<your-region>.amazonaws.com

# 作ったイメージにタグ付け
$ docker tag kyoto:latest <account-id>.dkr.ecr.<your-region>.amazonaws.com/kyoto:latest

# ECRにイメージをプッシュ
$ docker push <account-id>.dkr.ecr.<your-region>.amazonaws.com/kyoto:latest

# 上がったイメージサイズの確認(ECR上は8.08MBになった様子)
$ aws ecr describe-images \
  --profile <my-profile> \
  --repository-name kyoto \
  --query 'imageDetails[].imageSizeInBytes' --output text

8084683

4. ECS Fargateの作成

  • 確認ができたらすぐにリソースを削除して利用料を最小限にしたいので、作って簡単に消せるCloudFormationのテンプレート作成して進める
  • 後々のコスト確認のために各リソースのタグに Name: kyotoを指定
# stackの作成(10分程度)
$ aws cloudformation create-stack \
  --profile <my-profile> \
  --stack-name kyoto \
  --template-body file://kyoto-template.yaml \
  --capabilities CAPABILITY_IAM

# 状態確認(コマンドよりマネジメントコンソールの方がわかりやすい)
$ aws cloudformation describe-stacks \
  --profile <my-profile> \
  --stack-name kyoto
kyoto-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Fargate with ALB and VPC

Metadata:
  TimeoutInMinutes: 10
  AWS::CloudFormation::Interface:
    Tags:
      - Key: Name
        Value: kyoto

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: kyoto

  # Public Subnet 1
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: kyoto

  # Public Subnet 2
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [1, !GetAZs ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: kyoto

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: kyoto

  # Attach Internet Gateway to VPC
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Route Table
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: kyoto

  # Public Route
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Associate Public Subnets with Route Table
  SubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  SubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # ALB Security Group
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP traffic to ALB
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: kyoto

  # ALB
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: kyoto-alb
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Tags:
        - Key: Name
          Value: kyoto

  # ALB Target Group
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VPC
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /status
      HealthCheckPort: 8080
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      Matcher:
        HttpCode: 200
      Tags:
        - Key: Name
          Value: kyoto

  # ALB Listener
  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP

  # ECS Cluster
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: kyoto-cluster
      Tags:
        - Key: Name
          Value: kyoto

  # ECS Task Definition
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: kyoto-task
      Cpu: '256'
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt ECSExecutionRole.Arn
      ContainerDefinitions:
        - Name: kyoto-container
          Image: <image>
          Essential: true
          PortMappings:
            - ContainerPort: 8080
              HostPort: 8080
              Protocol: tcp
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: kyoto
      Tags:
        - Key: Name
          Value: kyoto

  # ECS Fargate Service
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: Listener # https://houdoukyokucho.com/2022/07/14/post-4158/
    Properties:
      Cluster: !Ref ECSCluster
      TaskDefinition: !Ref TaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          Subnets:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref ECSServiceSecurityGroup
      LoadBalancers:
        - ContainerName: kyoto-container
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroup
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 100
      Tags:
        - Key: Name
          Value: kyoto

  # ECSサービスのセキュリティグループ
  ECSServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow ECS traffic
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref ALBSecurityGroup
      Tags:
        - Key: Name
          Value: kyoto

  # Log Group
  LogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: "Delete" # default: Delete
    UpdateReplacePolicy: "Retain" # default: Delete
    Properties:
      LogGroupName: /ecs/kyoto
      RetentionInDays: 7
      Tags:
        - Key: Name
          Value: kyoto

  # ECS Execution Role
  ECSExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ECSExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:BatchCheckLayerAvailability
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - s3:GetObject
                  - ecr:GetAuthorizationToken
                Resource: '*'
      Tags:
        - Key: Name
          Value: kyoto

Outputs:
  VPCId:
    Description: The ID of the VPC
    Value: !Ref VPC

  PublicSubnet1Id:
    Description: The ID of the first public subnet
    Value: !Ref PublicSubnet1

  PublicSubnet2Id:
    Description: The ID of the second public subnet
    Value: !Ref PublicSubnet2

  ECSClusterName:
    Description: The name of the ECS Cluster
    Value: !Ref ECSCluster

  ECSServiceName:
    Description: The name of the ECS Service
    Value: !Ref ECSService

  TaskDefinitionArn:
    Description: The ARN of the ECS Task Definition
    Value: !Ref TaskDefinition

5. 後片付け

$ aws cloudformation delete-stack \
  --profile <my-profile> \
  --stack-name kyoto

6. 検証にかかったコスト

  • 0から試行錯誤しながらで $0.55
  • 為替を150円/$として 150 x 0.55 = 82.5円

Discussion