CloudFormationを使ってAWS上にDocker環境を構築する方法

AWS CloudFormationを使用すると、インフラストラクチャをコードとして定義し、AWSリソースのプロビジョニングとデプロイを自動化できます。この記事では、CloudFormationを使用してDockerコンテナを実行するための環境をAWS上に構築する方法を解説します。

目次

  1. CloudFormationとDockerの概要
  2. 準備作業
  3. 基本的なDockerホスト環境の構築
  4. ECSクラスターの構築
  5. ECRリポジトリの作成と活用
  6. より高度な構成例
  7. ベストプラクティスとヒント
  8. まとめ

CloudFormationとDockerの概要

CloudFormation はAWSのインフラストラクチャ・アズ・コード(IaC)サービスで、JSONやYAML形式のテンプレートファイルを使用してAWSリソースを定義・管理します。

Docker はコンテナ化技術の代表的なプラットフォームで、アプリケーションとその依存関係をコンテナにパッケージ化し、どこでも同じように実行できるようにします。

AWSでDockerを実行する主な方法:

  • EC2インスタンスにDockerをインストールして実行
  • Amazon ECS (Elastic Container Service)を使用
  • Amazon EKS (Elastic Kubernetes Service)を使用

この記事では、EC2インスタンスを使用した単一のDockerホストと、マネージドサービスであるECSを使用した方法の両方を紹介します。

準備作業

CloudFormationテンプレートを使用するための前提条件:

  1. AWSアカウント の取得
  2. AWS CLI のインストールと設定
  3. 適切なIAM権限 の設定
  4. 基本的なYAML/JSON の知識

基本的なDockerホスト環境の構築

まず、単一のEC2インスタンスにDockerをインストールして、基本的なコンテナホストを作成する方法を見ていきましょう。

CloudFormationテンプレート例(EC2+Docker)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for Docker host on EC2'

Parameters:
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t3.micro
    AllowedValues:
      - t3.micro
      - t3.small
      - t3.medium
    ConstraintDescription: Must be a valid EC2 instance type.
  
  KeyName:
    Description: SSH Key Pair name
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: Must be the name of an existing EC2 KeyPair.

  SSHLocation:
    Description: IP address range that can SSH to the EC2 instance
    Type: String
    Default: 0.0.0.0/0
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: Must be a valid CIDR range.

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

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: Docker-IGW

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

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

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

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet

  DockerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Docker host
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SSHLocation
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: Docker-SG

  DockerHost:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !GetAtt DockerSecurityGroup.GroupId
      KeyName: !Ref KeyName
      ImageId: ami-0c55b159cbfafe1f0  # Amazon Linux 2 AMI (example, replace with current AMI ID)
      SubnetId: !Ref PublicSubnet
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y
          amazon-linux-extras install docker -y
          service docker start
          systemctl enable docker
          usermod -a -G docker ec2-user
          # Install Docker Compose
          curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
          chmod +x /usr/local/bin/docker-compose
          # Create a simple Docker Compose file
          mkdir -p /home/ec2-user/docker-demo
          cat > /home/ec2-user/docker-demo/docker-compose.yml << 'EOL'
          version: '3'
          services:
            web:
              image: nginx:latest
              ports:
                - "80:80"
              restart: always
          EOL
          chown -R ec2-user:ec2-user /home/ec2-user/docker-demo
          # Start the container
          cd /home/ec2-user/docker-demo
          docker-compose up -d
          # Signal CloudFormation that the installation is complete
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource DockerHost --region ${AWS::Region}
      Tags:
        - Key: Name
          Value: Docker-Host

Outputs:
  InstanceId:
    Description: ID of the created EC2 instance
    Value: !Ref DockerHost
  
  PublicIP:
    Description: Public IP address of the Docker host
    Value: !GetAtt DockerHost.PublicIp
  
  WebsiteURL:
    Description: URL for the web server
    Value: !Sub http://${DockerHost.PublicIp}

デプロイ方法

  1. 上記のYAMLコードを docker-host.yml として保存します。
  2. AWS CLIを使用してスタックを作成します:
aws cloudformation create-stack \
  --stack-name docker-single-host \
  --template-body file://docker-host.yml \
  --parameters ParameterKey=KeyName,ParameterValue=your-key-pair-name \
  --capabilities CAPABILITY_IAM
  1. AWSマネジメントコンソールでスタックの作成進捗を確認します。

このテンプレートでは、以下のリソースを作成します:

  • VPC、サブネット、インターネットゲートウェイなどのネットワークリソース
  • セキュリティグループ(SSH、HTTP、HTTPSアクセスを許可)
  • DockerとDocker ComposeがインストールされたEC2インスタンス
  • NGINXコンテナを実行する簡単なDocker Compose設定

ECSクラスターの構築

次に、より本格的なコンテナオーケストレーションが必要な場合のために、Amazon ECS(Elastic Container Service)クラスターをCloudFormationで作成する方法を紹介します。

CloudFormationテンプレート例(ECS)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for ECS Cluster'

Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String
    Default: Docker-ECS-Demo

Resources:
  # VPC設定
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-VPC

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-IGW

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # サブネット
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-Public-Subnet-1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      CidrBlock: 10.0.2.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-Public-Subnet-2

  # ルーティング
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-Public-Routes

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

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

  # ECSクラスター
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${EnvironmentName}-cluster
      CapacityProviders:
        - FARGATE
        - FARGATE_SPOT
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Weight: 1

  # セキュリティグループ
  ECSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ECS tasks
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ECS-SG

  # ECSタスク実行ロール
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  # サンプルのECSタスク定義
  SampleTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${EnvironmentName}-sample-app
      Cpu: '256'
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: nginx
          Image: nginx:latest
          Essential: true
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: tcp
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref CloudWatchLogsGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: ecs

  # ログ
  CloudWatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /ecs/${EnvironmentName}
      RetentionInDays: 30

  # ALB
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: '30'
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref ECSSecurityGroup

  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref ALBTargetGroup
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      TargetType: ip
      Port: 80
      Protocol: HTTP
      UnhealthyThresholdCount: 5
      VpcId: !Ref VPC

  # ECSサービス
  SampleService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      ServiceName: !Sub ${EnvironmentName}-sample-service
      Cluster: !Ref ECSCluster
      TaskDefinition: !Ref SampleTaskDefinition
      DesiredCount: 2
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref ECSSecurityGroup
          Subnets:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
      LoadBalancers:
        - ContainerName: nginx
          ContainerPort: 80
          TargetGroupArn: !Ref ALBTargetGroup

Outputs:
  ClusterName:
    Description: The name of the ECS cluster
    Value: !Ref ECSCluster

  ALBDNSName:
    Description: DNS name of the load balancer
    Value: !GetAtt ALB.DNSName

  ServiceURL:
    Description: URL of the service
    Value: !Sub http://${ALB.DNSName}

デプロイ方法

  1. 上記のYAMLコードを ecs-cluster.yml として保存します。
  2. AWS CLIを使用してスタックを作成します:
aws cloudformation create-stack \
  --stack-name ecs-docker-cluster \
  --template-body file://ecs-cluster.yml \
  --capabilities CAPABILITY_IAM

このテンプレートでは以下のリソースを作成します:

  • VPC、サブネット、ルーティングなどのネットワークインフラストラクチャ
  • ECSクラスター(Fargateを使用)
  • サンプルのタスク定義とECSサービス
  • アプリケーションロードバランサー
  • 必要なIAMロールとセキュリティグループ

ECRリポジトリの作成と活用

カスタムDockerイメージを使用する場合は、Amazon ECR(Elastic Container Registry)にプライベートリポジトリを作成しておくと便利です。

CloudFormationテンプレート例(ECR)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for ECR Repository'

Parameters:
  RepositoryName:
    Type: String
    Default: docker-demo-repo
    Description: Name of the ECR repository

Resources:
  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref RepositoryName
      LifecyclePolicy:
        LifecyclePolicyText: |
          {
            "rules": [
              {
                "rulePriority": 1,
                "description": "Keep only the last 10 images",
                "selection": {
                  "tagStatus": "any",
                  "countType": "imageCountMoreThan",
                  "countNumber": 10
                },
                "action": {
                  "type": "expire"
                }
              }
            ]
          }
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowPull
            Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action:
              - ecr:GetDownloadUrlForLayer
              - ecr:BatchGetImage
              - ecr:BatchCheckLayerAvailability

Outputs:
  RepositoryURL:
    Description: The URL of the ECR repository
    Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}

ECRリポジトリの使用方法

  1. 上記のテンプレートをデプロイして、ECRリポジトリを作成します。
  2. AWS CLIを使用してECRにログインします:
aws ecr get-login-password --region <リージョン> | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com
  1. ローカルのDockerイメージにタグを付けます:
docker tag myapp:latest <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/docker-demo-repo:latest
  1. イメージをECRにプッシュします:
docker push <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/docker-demo-repo:latest
  1. ECSタスク定義でこのイメージを参照します:
ContainerDefinitions:
  - Name: myapp
    Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-demo-repo:latest
    # その他の設定...

より高度な構成例

ECSクラスターのオートスケーリング

CloudFormationを使用してECSサービスのオートスケーリングを設定できます:

ScalableTarget:
  Type: AWS::ApplicationAutoScaling::ScalableTarget
  Properties:
    MaxCapacity: 10
    MinCapacity: 2
    ResourceId: !Sub service/${ECSCluster}/${SampleService.Name}
    ScalableDimension: ecs:service:DesiredCount
    ServiceNamespace: ecs
    RoleARN: !GetAtt AutoScalingRole.Arn

ScalingPolicy:
  Type: AWS::ApplicationAutoScaling::ScalingPolicy
  Properties:
    PolicyName: CPUScalingPolicy
    PolicyType: TargetTrackingScaling
    ScalingTargetId: !Ref ScalableTarget
    TargetTrackingScalingPolicyConfiguration:
      PredefinedMetricSpecification:
        PredefinedMetricType: ECSServiceAverageCPUUtilization
      TargetValue: 70.0
      ScaleInCooldown: 60
      ScaleOutCooldown: 60

ECS Anywhere(オンプレミスサーバーの使用)

オンプレミスサーバーをECSクラスターに登録するための設定も可能です:

ECSAnywhereRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            Service: ssm.amazonaws.com
          Action: 'sts:AssumeRole'
    ManagedPolicyArns:
      - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
      - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role'

マルチコンテナアプリケーション

複数のコンテナを含むタスク定義の例:

WebAppTaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: web-app
    Cpu: '512'
    Memory: '1024'
    NetworkMode: awsvpc
    RequiresCompatibilities:
      - FARGATE
    ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
    ContainerDefinitions:
      - Name: web
        Image: nginx:latest
        Essential: true
        PortMappings:
          - ContainerPort: 80
            HostPort: 80
        DependsOn:
          - ContainerName: api
            Condition: START
      - Name: api
        Image: node:14-alpine
        Command: ["node", "app.js"]
        Essential: true
        PortMappings:
          - ContainerPort: 3000
        Environment:
          - Name: DB_HOST
            Value: !GetAtt Database.Endpoint.Address
      - Name: sidecar
        Image: datadog/agent:latest
        Essential: false
        Environment:
          - Name: DD_API_KEY
            Value: !Ref DatadogApiKey

ベストプラクティスとヒント

セキュリティのベストプラクティス

  1. 最小権限の原則:IAMロールには必要最小限の権限のみを付与
  2. コンテナイメージのスキャン:ECRのイメージスキャン機能を有効化
  3. シークレット管理:AWS Secrets Managerを使用して機密情報を管理
  4. セキュリティグループの適切な設定:必要なポートのみを開放

コスト最適化

  1. Fargate Spotの活用:耐障害性のあるワークロードにはFargate Spotを使用
  2. リソースサイジングの最適化:CPUとメモリを適切に設定
  3. オートスケーリングの活用:需要に応じたリソースの拡大縮小
  4. 不要なリソースの削除:使用していないリソースはCloudFormationスタックごと削除

運用の効率化

  1. CI/CDパイプラインの構築:AWS CodePipelineを使用した自動デプロイ
  2. モニタリングとログ:CloudWatchと連携して可視性を確保
  3. モジュラー設計:CloudFormationのネストされたスタックを活用
  4. Infrastructure as Code(IaC)の徹底:手動変更を避け、すべての変更をコードで管理

まとめ

この記事では、CloudFormationを使用してAWS上にDockerコンテナ環境を構築する方法を解説しました。単一のEC2インスタンスにDockerをインストールする基本的なアプローチから、ECSクラスターを使用した本格的なコンテナオーケストレーションまで、様々な構成例を紹介しました。

CloudFormation テンプレートを使用することで、インフラストラクチャをコードとして管理し、再現性の高い環境を簡単に構築できます。また、環境の変更や更新も容易になり、バージョン管理やチームでの共有も可能になります。

AWS上のDockerコンテナ環境は、アプリケーションの開発からテスト、本番デプロイまで、柔軟でスケーラブルなインフラストラクチャを提供します。CloudFormationと組み合わせることで、そのような環境の管理と自動化が実現できます。

次のステップとして、提供したテンプレートを自分のニーズに合わせてカスタマイズし、CI/CDパイプラインを構築して、コンテナ化されたアプリケーションのデプロイプロセスを自動化することをお勧めします。

コメント

タイトルとURLをコピーしました