ECS Fagate, ECR, RDS の導入についての備忘録|基礎インフラ【3】

クジラ

皆さんこんにちは、小幡です。

この記事は、基礎インフラ備忘録シリーズの第3回目です。2回目は以下のリンクから参照ください。

前回はEC2インスタンスの作成をCloudFormationで自動化してみました。

今回はDockerでコンテナイメージを作成しECRへプッシュ、CloudFormationでECS, RDSの導入をしてみます。

CloudFormationへ構成の記述をしていくのですが、前回のテンプレートよりも大分複雑に見えると思いますが、気長に頑張りましょう。それぞれのリソースについては、ECSとコンテナ化において、外すことのできないものでものです。

自分自身への備忘録的な記事なので、検証を行っていないところや、間違いもあると思いますのでご注意ください。

Dockerでイメージをビルドする

最終的にはDockerで作成したイメージはECRにプッシュします。

そのプッシュ先のECRで、「リポジトリを作成」をします。AWSマネジメントコンソールで作業を行います。

次に、Docker が ECR にアクセスできるように、以下のコマンドで認証トークンを取得してログインを行います。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

成功を確認したら、次のコマンドでDockerイメージをビルドします。

docker build -t my-app .

ECR のリポジトリ URL に合わせて、ビルドしたイメージに新しいタグを付けます。

docker tag my-app:latest <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest

最後に、タグ付けしたイメージを AWS 上のリポジトリへ送信します。

docker push <あなたの12桁のAWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest

これでECRの準備は完了しました。

ここまでの作業はローカルで開発したDocker上のソースを、最終的に本番環境でリリースしたり、誰かに共有した場合に行う手順です。万が一ローカルのDockerが壊れてしまっても、クラウド上にあるので安心ですね

RDSの導入

上記でDockerイメージをビルドしていますが、これの前にRDSの導入も進めました。

CloudFormationと合わせてsettings.pyの修正も必要なのですが、結果的に以下のような内容に変わっています。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': '5432',
    }
}

それぞれの値には環境変数を使用しています。こうすることで、ローカルでの作業と本番でのリリースは、それぞれ異なる環境変数を与えることで正常に動作します。

CloudFormationでECS Fagateを導入

今回も新しくテンプレートファイルを用意します。結果的には以下の状態になりました。

AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Fargate infrastructure with ALB and RDS

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Select your Default VPC

  SubnetIdX:
    Type: AWS::EC2::Subnet::Id
    Description: Select Subnet A (e.g. ap-northeast-1a)

  SubnetIdY:
    Type: AWS::EC2::Subnet::Id
    Description: Select Subnet B (e.g. ap-northeast-1c)

  ImageUri:
    Type: String
    Default: "710496666739.dkr.ecr.ap-northeast-1.amazonaws.com/my-django-app:latest"

Resources:
  # --- ECS Cluster ---
  MyECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: MyDjangoCluster

  # --- CloudWatch Logs ---
  MyLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/my-django-app
      RetentionInDays: 7

  # --- IAM Role ---
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      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

  # --- ECS Task Definition ---
  MyTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: my-django-task
      Cpu: '256'
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: django-container
          Image: !Ref ImageUri
          Environment:
            - Name: DB_HOST
              Value: !GetAtt MyDBInstance.Endpoint.Address
            - Name: DB_NAME
              Value: dbname1234
            - Name: DB_USER
              Value: dbuser5678
            - Name: DB_PASSWORD
              Value: dbpassword9012
          PortMappings:
            - ContainerPort: 8000
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref MyLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: ecs

  # --- Application Load Balancer ---
  MyALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      SecurityGroups: [ !GetAtt ALBSecurityGroup.GroupId ]
      Subnets: [ !Ref SubnetIdX, !Ref SubnetIdY ]

  MyTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VpcId
      Protocol: HTTP
      Port: 8000
      TargetType: ip
      HealthCheckPath: /

  MyALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref MyALB
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref MyTargetGroup

  # --- ECS Service ---
  MyECSService:
    Type: AWS::ECS::Service
    DependsOn: MyALBListener
    Properties:
      Cluster: !Ref MyECSCluster
      TaskDefinition: !Ref MyTaskDefinition
      DesiredCount: 1
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups: [ !GetAtt ContainerSecurityGroup.GroupId ]
          Subnets: [ !Ref SubnetIdX, !Ref SubnetIdY ]
      LoadBalancers:
        - ContainerName: django-container
          ContainerPort: 8000
          TargetGroupArn: !Ref MyTargetGroup
      HealthCheckGracePeriodSeconds: 300

  # --- Security Groups ---
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP to ALB
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - { IpProtocol: tcp, FromPort: 80, ToPort: 80, CidrIp: 0.0.0.0/0 }

  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow ALB to Container
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8000
          ToPort: 8000
          SourceSecurityGroupId: !GetAtt ALBSecurityGroup.GroupId

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow access to RDS from ECS
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !GetAtt ContainerSecurityGroup.GroupId

  # --- RDS Instance ---
  MyDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet group for RDS
      SubnetIds: [ !Ref SubnetIdX, !Ref SubnetIdY ] # プロパティ名を修正

  MyDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBName: mydatabase
      AllocatedStorage: '20'
      DBInstanceClass: db.t4g.micro
      Engine: postgres
      MasterUsername: dbuser
      MasterUserPassword: dpassword123
      DBSubnetGroupName: !Ref MyDBSubnetGroup
      VPCSecurityGroups: [ !GetAtt DBSecurityGroup.GroupId ]
      PubliclyAccessible: false

上記のテンプレートは前回作成したものと、使用しているリソースが違います。

またECSの使用のためにALBの設定なども制約に従い定義していますので、少し複雑に感じられると思います。

実際私も、上記のようなサンプルのテンプレートを最初に見たときは、書いてあることを読み解くだけでもだいぶ時間がかかりましたし、予想通りテンプレートの実行ではスタックが失敗となることが何度もあり、少し大変な思いをしました。

EC2単体での作成と、コンテナ化の作業は大きく違うものとなっていますので、まずはRDSを使わない最小構成で実行してみるのも良いかもしれません。

実際私は、RDSの追加はECSの作成がほとんど成功し、失敗した理由がすでに定義済みであったDBへの接続が失敗していたという状況を確認してから行っています。

またRDS、ALB、ECSの作成にはそれぞれ2分~5分前後の時間がかかっていたように感じます。スタックのプレビューに正確な作成にかかった時間が確認できるのですが、見ていませんでした。なので、一回で全ての実行を試すと、結果が得られるまでとても時間がかかるので、そういう意味でも1つずつテンプレートに記述して実行してみるのが良いと思います。

スタックのタイムラインビュー(作成)

RDS, ALB, ECSが作成されるまでにかかった時間を見てみましょう。

AWSマネジメントコンソールで以下の場所から確認できます。

CloudFormation > スタック > [作製したスタック名] > 上部の「イベント」タブ > 右上の「タイムラインビュー」

これを見ると、どのリソースがどんな順番で、どのくらい時間がかかったかが一目瞭然ですね。

目を引くのが、ALBの作成が約3分、次にRDSの作成が約6分、最後にECSの作成が1分半です。詳細は以下の通りです。

AWSリソース名合計時間(秒)
ALB187
RDS371
ECS94

余談ですが、ECSのヘルスチェックで失敗となる場合は、作成中から失敗になるまでもっと時間がかかっていた印象です。

私の場合はECSまで直接確認しに行って、失敗を確認したので、スタックが作成中の状態でしたが、スタックの削除を実行していました。

スタックのタイムラインビュー(削除)

ECSとRDSは起動したままにしておくと、EC2単体で起動していた以上に料金が高くなりますので、テストが完了したら削除するようにしています。早く新しくサービスを開発して、運用などを本格的に行い実験してみたいところです。

削除にもそれなりに時間がかかることがわかりました。(左側半分が作成時間、右側半分が削除時間です。)

今回の削除の際は、ECSが一番時間がかかっていたようですね。

まとめ

今回はCloudFormationのテンプレートにECS, ALB, RDSなどのリソースについて記述し、コンテナでのサービス開発を想定した構成とすることができました。

今回のCloudFormationではスタックの作成と削除で時間がかかっていますが、基本的にはインフラ構成を頻繁に作成したり削除することは少なく、一度整ってしまえば運用で触ることはあまりないものと考えています。それよりもローカルのDockerイメージを再ビルドしたらECRへプッシュして、それをこのインフラ構成に反映させるという作業が入ってきます。

コンテナ化ができたので、次回はローカルでコンテナ内のサービスに機能を追加し、その後リリースをするという事を想定しCI/CDの観点を導入していきたいと思います。

このCI/CDまでを導入できれば、『エクストリームプログラミング』で学ぶことができるプラクティスを実践できる構成としてサービス開発に集中できる状態になれると考えています。

以上です。

この記事を書いた人

小幡 知弘

1990年茨城県神栖市生まれ
2013年大阪芸術大学卒業
Python×Webエンジニア