docker基盤としてECSを採用しました③

こんにちはmatsです。

少し間が空いてしまいましたが、引き続きECSまわり記事です。

cloud-init

IMではSpot FleetでECSの基盤を構築しているのですが、その際に重要なのが cloud-init になります。ECS Optimized AMIにcloud-initで最低限の設定を行うことで、Ansible等での構成管理やAMIの管理工数を削減しています。今回はそのcloud-initについてお話します。

cloud-config

まず、記述方法についてです。ECSのドキュメントなど多くの記事でBash形式の記述が取り上げられていますが、cloud-config 形式のほうが圧倒的におすすめです。IMはAmazon Linux (ECS Optimized AMI)なので、他のディストロでは挙動が異なる可能性はありますが、Bash形式よりも細かく挙動を制御することが出来ます。

S3 からの読み込み

cloud-init の設定はS3に配置し、http経由で読み込むようにしています。 理由としては、AutoScaling や SpotFleet でcloud-initの設定内容を変更するためには、設定の作り直しが必要で、運用上不便な点が多いからです。 権限は VPC Endpoint で与えています。 複数ファイル記述することも出来るので、用途に応じて分割すると良いかと思います。

#include
https://s3-ap-northeast-1.amazonaws.com/<bucket>/test1.yml
https://s3-ap-northeast-1.amazonaws.com/<bucket>/test2.yml

cloud-init で行っている設定

ユーザ作成

#cloud-config

users:
  - default
  - name: matsuda
    lock_passwd: true
    gecos: Kazuki Matsuda
    groups: wheel,docker
    sudo: [ "ALL=(ALL) NOPASSWD:ALL" ]
    shell: /bin/bash
    ssh-authorized-keys: [ "ssh-rsa AA ... DQ==" ]

docker のセキュリティ的にはログイン可能なユーザは作らないほうが良いのですが、過渡期なので docker の操作ができるユーザを作っています。

なお、 - default で ec2-user を作成しているらしいので注意が必要です。

AutoScaling環境でのユーザ管理でもAMIの作り直しなどが不要になるので、ユーザ管理の方法としてはかなりイケてるんじゃないでしょうか。

Spot Instance の中断検知

#cloud-config

packages:
  - aws-cli
  - jq

write_files:
  - path: /etc/ecs/deregister.sh
    permissions: '0755'
    content: |
        #!/bin/sh
        region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//g')
        ecs_cluster=$(curl -s http://127.0.0.1:51678/v1/metadata | jq -r '.Cluster')
        ecs_instance=$(curl -s http://127.0.0.1:51678/v1/metadata | jq -r '.ContainerInstanceArn')
        for i in {1..11}
          do
            if curl -s http://169.254.169.254/latest/meta-data/spot/termination-time | grep -q .*T.*Z; then
              aws ecs deregister-container-instance --region $region --cluster $ecs_cluster --container-instance $ecs_instance --force
            fi
            sleep 5
          done

  - path: /etc/cron.d/spot_retire
    content: |
        PATH=/sbin:/bin:/usr/sbin:/usr/bin
        * * * * * root /etc/ecs/deregister.sh

弊社では Spot Instance をヘビーに使っているのですが、ECSの基盤として利用する際は中断処理を検知してコンテナをオフロードする必要があります。 こちらの設定も cloud-init で行っております。

5秒おきに meta-data を参照し、中断の時刻が取得できた場合はECS Clusterから退役するようなcronを設定しております。

Instance Store (SSD) のマウント

#cloud-config

bootcmd:
  - [ cloud-init-per, instance, docker_storage_setup, /usr/bin/docker-storage-setup ]
  - |
      yum install -y mdadm
      for i in 0 1 2 3 4 5 6 7 ; do
          disk=/dev/nvme${i}n1
          dir=/mnt/nvme${i}
          if [ -e "$disk" ]; then
              mkfs.ext4 -F $disk
              mkdir -p $dir
              mount $disk $dir
              rm -rf $dir/*
          fi
      done
      chmod -R 777 /mnt

dokcer上で Instance Store を利用するには、docker engineの起動前にファイルシステムのマウントをする必要があります。 これを実現するに弊社では bootcmd ステップで処理を行っております。 なお、ひとつめの

  - [ cloud-init-per, instance, docker_storage_setup, /usr/bin/docker-storage-setup ]

は、Docker Storage の初期化を行っているようで、 /etc/cloud/cloud.cfg.d/90_ecs.cfg からコピーしてきて記述しております。 (merge_how が記述されていないので、上書きしてしまうようです。)

その他

#cloud-config

write_files:
  - path: /etc/sysctl.d/12-docker.conf
    permissions: '0644'
    content: |
        net.core.somaxconn = 65535
        net.core.netdev_max_backlog = 16384
        net.ipv4.ip_local_port_range = 1024 65535
        net.ipv4.tcp_tw_reuse = 1
        net.ipv4.tcp_max_syn_backlog = 65535
        vm.swappiness = 1

  - path: /etc/cron.d/reboot_ecs-agent
    content: |
        PATH=/sbin:/bin:/usr/sbin:/usr/bin
        0 * * * * root docker kill ecs-agent

runcmd:
  - sysctl -p /etc/sysctl.d/12-docker.conf

merge_how: 'list(append)+dict(recurse_array)+str()'

あとは、

  • カーネルパラメータのチューニング
  • ecs-agentが詰まることがあるので、定期的な再起動
  • cloud-config が複数あった場合の結合方法

などの設定を行っております。

まとめ

上記のような設定を clous-init 用のリポジトリで管理しており、GitHub の Pull Request に連動する形でS3へコピーしております。

これらが、弊社で行っているECSに適した形の構成管理になります。Ansible等を使うのもいいですが、変更箇所が少ない場合は cloud-init の方が都合がいい部分も多いのでおすすめです。