Amazon EC2 Auto Scaling を Terraform で素振り

Amazon EC2 Auto Scaling を ユーザーガイド を読みながら Terraform で素振りしたメモ。残骸はこちら

Launch Configuration と Launch Template

Auto Scaling で起動する EC2 インスタンスを定義する方法は下記の複数があります。

  • Launch Configuration
  • Launch Template

Launch Configuration より Launch Template のほうが後発で、以下のような違いがあります。

  • Launch Configuration は Auto Scaling 専用なのに対して Launch Template は他の用途にも使用できる
    • Launch Template から直で EC2 インスタンスを作成したりできる
  • Launch Template はバージョン管理されるので更新すると新たにバージョンが作成される
    • Launch Configuration はバージョン管理されないので更新時に新旧維持するためには別名で作成するしか無い

また、Launch Template でしかできないことがいくつかあります。基本的に Launch Template を使っていれば良さそうです。

ルートデバイス

Launch Configuration なら Terraform で root_block_device でルートデバイスが指定できますが Launch Template だと block_device_mappings でデバイス名を明示的に指定する必要があります。

最初 CentOS 向けに /dev/sda1 で Launch Template を作成していましたが、AMI を Amazon Linux 2 ベースに変更したところインスタンスが開始できなくなりました。Amazon Linux 2 だとルートデバイスが /dev/xvda になるためです。

トラブルを避けるために Launch Template で block_device_mappings でルートデバイスを指定していろいろ設定するのはやめて、あらかじめカスタムも AMI にブロックデバイスの情報を程よく刻んでおくのが良いかもしれません。もっともそんな間違いは滅多に無いだろうし、間違えたとしても直ぐに気づくだろうので気にしなくても良いかもしれません。

下記のように AMI の block_device_mappings を参照すれば良いかも。

data "aws_ami" "app" {

  // snip

}

resource "aws_launch_template" "app" {

  // snip

  block_device_mappings {
    device_name = "${lookup(data.aws_ami.app.block_device_mappings[0], "device_name")}"

    ebs {
      volume_size           = 40
      volume_type           = "gp2"
      delete_on_termination = true
    }
  }
}

Blue/Green Deployment

無停止でインスタンスの AMI を更新する方法はいくつか考えられます。

  • ELB までをワンセットにした Blue/Green Deployment で DNS を切り替える
  • AutoScaling Group までをワンセットにした Blue/Green Deployment で ELB を切り替える
  • Launch Configuration/Template までをワンセットにして AutoScaling Group 内のインスタンスを Rolling Update

最後の方法は Terraform 単体ではたぶん無理です。DNS を切り替える方法は TTL の影響を受けるし Route53 も必要なので、ELB を切り替える方法を試しました。

AutoScaling Group が参照する Launch Configuration を変更したとしても起動済の EC2 インスタンスには影響しません。その AutoScaling Group で新しく起動するインスタンスでのみ新しい Launch Configuration で起動します。なので AMI 更新時に AutoScaling Group はリソースが再作成されるようにする必要があります。これは AutoScaling Group の name に Launch Configuration の name を含めればよいです、後述の通り Launch Configuration の変更時には別名で再作成させるため、Launch Configuration の name が変われば AutoScaling Group の name も変わって、AutoScaling Group も再作成になります。

AutoScaling Group が再作成されるとき、デフォだと「古いリソースの削除→新しいリソースの作成」の順番で処理されます。これだと再作成するときに一時的にインスタンスが全滅してしまうので lifecyclecreate_before_destroy = true を指定して「新しいリソースの作成→古いリソースの削除」の順番で処理されるようにします。

Launch Configuration は作成後に更新ができず、変更があるときはリソースの再作成が必要になります。再作成時に lifecyclecreate_before_destroy = true を指定していると一時的に新旧のリソースが同時に存在するため名前の重服を避けるための name_prefix を設定します。これで Launch Configuration の name は Terraform がタイムスタンプを元に自動生成します。

AutoScaling Group と ELB のターゲットグループは aws_autoscaling_grouptarget_group_arns で行う必要があります。aws_autoscaling_attachment リソースでも紐付けできますが、aws_autoscaling_attachment だとデプロイ時に一時的に 503 になる期間が生じます。aws_autoscaling_attachment だと新しい AutoScaling Group が作成された後に ELB にアタッチされるため、ヘルスチェックのタイプが作成直後は EC2 になります。そのため EC2 インスタンスのステータスが OK になった時点で 古い AutoScaling Group が ELB からデタッチされるため、その後、新しい AutoScaling Group を ELB にアタッチしして新しい EC2 インスタンスで Web サーバの準備が終わって ELB のヘルスチェックが完了するまでの期間がダウンタイムになります。

まとめると、次のようなリソース定義になります。

resource "aws_launch_configuration" "app" {
  name_prefix     = "hello-asg-app-"

  // snip

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "app" {

  name                 = "${aws_launch_configuration.app.name}"
  launch_configuration = "${aws_launch_configuration.app.name}"

  // snip

  health_check_type   = "ELB"
  target_group_arns = ["${aws_lb_target_group.app.arn}"]

  lifecycle {
    create_before_destroy = true
  }
}

なお、この方法だとひとつの ELB に新旧両方の AutoScaling Group がアタッチされている期間があるため、ELB からの転送先に一時的に新旧が混在します。

Launch Template を使う場合は Launch Template がバージョニングをサポートしているので、aws_launch_configurationname_prefixcreate_before_destroy のような仕込みは不要です。ただし Launch Template の更新時に AutoScaling Group の再作成を行わせる必要があるのは変わらないので、AutoScaling Group の name には Launch Template のバージョンを含めます。

resource "aws_autoscaling_group" "app" {

  name = "hello-asg-app-${aws_launch_template.app.latest_version}"

  launch_template = {
    id      = "${aws_launch_template.app.id}"
    version = "${aws_launch_template.app.latest_version}"
  }

  health_check_type   = "ELB"
  target_group_arns = ["${aws_lb_target_group.app.arn}"]

  lifecycle {
    create_before_destroy = true
  }
}

スケーリングポリシー

メトリクスに基づいた、いわゆるオートスケーリングの設定には下記の3つのポリシーが使用できます。

  • ターゲット追跡スケーリング
  • ステップスケーリング
  • 簡易スケーリング

ターゲット追跡スケーリングが一番後発です。基本的にターゲット追跡スケーリングを使っておけば OK です。

ターゲット追跡スケーリング

指定したメトリクスが指定した値に近づくように自動的にスケーリングします。メトリクスには以下の事前定義されたメトリクスが指定できます。

  • ターゲット別の Application Load Balancer リクエストの数
  • CPU の平均使用率
  • 平均ネットワーク入力 (バイト)
  • 平均ネットワーク出力 (バイト)

スケーリングポリシーを設定すると、マネジメントコンソールからは変更や削除ができない CloudWatch アラームが自動で作成されます。例えば「CPU の平均使用率」を 40 に設定すると次のような CloudWatch アラームが自動的に作成されます。

  • CPUUtilization > 40 for 3 datapoints within 3 minutes
  • CPUUtilization < 36 for 15 datapoints within 15 minutes

この2つの閾値に収まるようにインスタンス数が AutoScaling Group の Min と Max の間で自動的に調整されます。さらに CPUUtilization < 36 のアラームはCPU使用率がほぼゼロのまま放置しているとさらに低い値に自動的に変化したので、スケーリング閾値のアラーム自体も自動で調整されるようです。

マネジメントコンソールからは事前定義済の前述の4つのメトリクスしか選択できませんが、任意のメトリクスが指定できます。ただし、AutoScaling Group のインスタンス数に対して逆相関(インスタンスが増えれば値が減る)なメトリクスでなければ意味がありません。

Terraform で事前定義済のメトリクスを指定するには次のようにします。

resource "aws_autoscaling_policy" "cpu" {
  autoscaling_group_name = "${aws_autoscaling_group.app.name}"
  name                   = "cpu"
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }

    target_value = 40.0
  }
}

同じメトリクスをカスタムメトリクスとして指定するなら以下のようになります。

resource "aws_autoscaling_policy" "cpu" {
  autoscaling_group_name = "${aws_autoscaling_group.app.name}"
  name                   = "cpu"
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    customized_metric_specification {
      namespace   = "AWS/EC2"
      metric_name = "CPUUtilization"
      statistic   = "Average"

      metric_dimension {
        name  = "AutoScalingGroupName"
        value = "${aws_autoscaling_group.app.name}"
      }
    }

    target_value = 40.0
  }
}

簡易スケーリングとステップスケーリング

簡易スケーリングとステップスケーリングは、スケーリングポリシーと、スケーリングポリシーをアクションとして呼び出す CloudWatch アラームを別々に作成する必要があります。

簡易スケーリングとステップスケーリングはよく似ていて、簡易スケーリングがスケーリングの調整値(インスタンスをどれだけ追加するか、とか)が1段階しか設定できないのに対して、ステップスケーリングならアラームを超過した量に応じて、段階的にスケーリング調整値を指定できます(閾値を超えたら 4 台追加するけれども 20% 超えてるなら一気に 10 台追加する、とか)。

なお、ステップスケーリングのみ metric_aggregation_type が指定できます、が、何に使われるものなのかわかりません・・・。対応するアラームの statistic と同じものを指定しておけばよいのかな? なお、API リファレンスでは metric_aggregation_type には Minimum/Maximum/Average のいずれかしか指定できないことになっているのですが実際には Sum も指定できるようです。SampleCount は指定できないようです。

スケジュールに基づくスケーリング

指定された間隔や cron 式による時刻に AutoScaling Group の Desired Capacity や Min Size や Max Size を変更します。特定の曜日や特定の時間帯だけスケールインさせたりできます。

詳細モニタリング

スケーリングポリシーを設定するなら、詳細モニタリングを有効にして1分ごとにメトリクスが記録されるようにし、迅速にスケーリングできるようにしておくと良いです。

resource "aws_launch_template" "app" {

  // snip

  monitoring {
    enabled = true
  }
}

クールダウンとウォームアップ

クールダウンは手動スケーリングや簡易スケーリングポリシーで適用される。クールダウン期間中は簡易スケーリングポリシーによるスケーリングが発生しなくなる。

手動スケーリングではクールダウンをなしにするか Group に設定されたデフォルトのクールダウン期間を用いるかを選択できる。簡易スケーリングポリシーではスケーリング固有のクールダウン期間を指定するか、Group に設定されたデフォルトのクールダウン期間を用いるかを選択できる。

ウォームアップはターゲット追跡スケーリングとステップスケーリングで適用される。ウォームアップ期間中のインスタンスはスケールアウトの調整値を適用するときの元数として使用されない。例えば、現在5台のインスタンスが起動中で、うち2台がウォームアップ期間だとすると、このときにインスタンスを3台追加する調整値のスケーリングポリシーが実行されたとしても、2台がウォームアップ中なので1台しか追加されない。

ライフサイクルフック

ELB にアタッチされる AutoScaling Group の場合は前述の通り ELB の転送先の切り替えによる Blue/Green Deployment で無停止でアップデートできます。

Terraform が AutoScaling Group を再作成するとき、Terraform は新しい AutoScaling Group が利用可能になるまで待機してから古い AutoScaling Group を削除します。AutoScaling Group のヘルスチェックタイプを ELB にすれば AutoScaling Group が利用可能かの判断は ELB のヘルスチェックが Healthy となったときになるため、Terraform が AutoScaling Group を再作成するとき、新しいインスタンスが ELB で Healthy になるまで Terraform は待機します。

一方、ELB が絡まないワーカーのようなインスタンスだと前述のような ELB のヘルスチェックが使えないため簡単ではありません。EC2 インスタンスのステータスが OK となった時点で Terraform が新しい AutoScaling Group を利用可能として待機が終わってしまうためです。

ライフサイクルフックを使えばインスタンスのステータスが OK となっただけでは利用可能とはならず、準備が整った任意のタイミングまでインスタンスが利用可能と判断されるのを待機できるので、ワーカーのようなインスタンスでも Blue/Green Deployment できます。

例えば、下記のようなスクリプトをユーザーデータに設定し、インスタンスの開始後に準備が整うまで利用可能になるのを待機させることができます。

#/bin/bash

set -eux

# インスタンスメタデータからインスタンスIDとリージョンを取得
instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/.$//')

# インスタンスのタグから AutoScaling Group を取得
group_name=$(aws ec2 describe-instances \
  --region "$region" \
  --instance-ids "$instance_id" \
  --query 'Reservations[].Instances[].Tags[?Key==`aws:autoscaling:groupName`][].Value' \
  --output text)

# ここでなにか処理して準備ができるまで待つ

# complete-lifecycle-action を呼び出してライフサイクルを続行する
# この処理が呼ばれるまでは新しいインスタンスは利用可能とならないので
# Terraform も待機される
aws autoscaling complete-lifecycle-action \
  --region "$region" \
  --auto-scaling-group-name "$group_name" \
  --lifecycle-hook-name "hello-asg-app-launching" \
  --lifecycle-action-result "CONTINUE" \
  --instance-id "$instance_id"

ただ、インスタンスが生きたままインスタンスの中のサービスが死んでも AutoScaling による故障が検出されないので、ヘルスチェックも独自に実装して適宜 aws autoscaling set-instance-health でインスタンスを Unhealthy にするか、software watchdog timer などでインスタンスを停止するなどの対策が必要になるかもしれません。

Application AutoScaling

今回試したものは EC2 インスタンスをスケーリングするための EC2 Auto Scaling ですが、その他の AWS リソースのスケーリングのための Application AutoScaling というものもあります。

Packer

カスタム AMI を作成するために Packer を使いました。手作業や AWS CLI で頑張るよりはだいぶ楽だと思います。それでも Docker イメージを作成するのと比べればそうとう手間ですが。

さいごに

頻繁に更新されるシステムで都度 AMI を作り直してデプロイするのは時間もかかるし検証も面倒そうなので、AMI を更新するのはミドルウェア構成が変わったときだけにして、アプリのソースは tar.gz で固めて S3 に置くなどしてインスタンスの開始時に S3 から展開してサービスを開始、アプリの更新時は AMI はそのままでアプリのソースだけ S3 経由でデプロイするのが運用しやすそうです(ミドルウェアレイヤーだけイミュータブルなイメージ)。

また、スケーリングポリシーはそれが必要となるようなシステムに成長するかどうかはやってみなければわからないだろうので最初は設定せず、最初は必要に応じて手動スケーリングで良いだろうと思います。EC2 インスタンスは AWS 側の都合でわりと突然死することがあるので AutoScaling でインスタンス数の維持(故障したら自動で再作成)だけでも十分有用です。

ただ、AutoScaling のために AMI をうまく作るために変に消耗するぐらいなら、ECS とかで Docker イメージをアプリのコード込でデプロイするのでも良いように思います。

Amazon Elastic Container Service for Kubernetes (EKS) を Terraform で素振り

少し前に EKS が Tokyo リージョンで使えるようになったので素振りしたメモ。残骸はこちら

なお、てっきり EKS も Fargate が使えるものだと思っていたのですが、そうでもなかったので(将来的にはできるようになる?)、EC2 でワーカーノード作ってます。

EKS の費用

EKS クラスタ を作成すると 1 時間あたり 0.20 USD、さらにクラスタのワーカーノードのために EC2/EBS の料金、あと Service で LoadBalancer を指定すると ELB の費用も必要です。

kubectl と aws-iam-authenticator

EKS を使用するためには Kubernetes の kubectl コマンドと、kubectl で IAM 認証を使うための aws-iam-authenticator をインストールする必要があります。

kubectl は Kubernetesの公式ドキュメント に則ってディストリビューションのパッケージマネージャーでインストールしてもよいし、AWSでバイナリがホストされている のでそれを直でインストールしても良いです。

aws-iam-authenticator も AWS にバイナリがホストされているのでそこからインストールします。

クラスタを作成

EKS クラスタの作成には結構な時間がかかります(8分ぐらいかかった)。

ECS クラスタは一瞬で作成されていましたが、ECS の場合はなにかインスタンス的なものが作られているわけではないのに対して、EKS クラスタの場合は Kubernetes のコントロールプレーン(Kubernetes Master)のインスタンスが実際にプロビジョニングされるためです(たぶん)。ELB や RDS と同類のものだと思えば良いですね。

なお、始めにお試しでマネジメントコンソールから作成してみたところ下記の記事のような問題で kubectl が失敗したので AWS CLI で作成するか Terraform などで作成するのが良さそうです。

タグ

EKS で使用する VPC・サブネット・EC2インスタンス には kubernetes.io/cluster/${cluster_name} のような名前のタグが必要です。このタグを目印に EKS がこれらを制御するようです。

shared はその VPC やサブネットが複数のクラスタから使用されることを許可します。値 owned はそのクラスタ専用になるようですが、どういう違いがあるのかはよくわかりませんでした。

なお、EKS のユーザーガイドにはそのような記述が無いようなのですが、ワーカーノードにアタッチするセキュリティグループにも同様にタグが必要です。

コントロールプレーンのセキュリティグループ(クラスタ作成時に指定するセキュリティグループ)ではなく、EC2インスタンスにアタッチするセキュリティグループです。Kubernetes が ELB を自動的に作成した際に ELB のセキュリティグループからワーカーノードのセキュリティグループへアクセス可能にするために、ワーカーノードのセキュリティグループが Kubernetes によって自動的に更新されるため、そのためにタグ付けが必要です。

タグがないと、ELB の作成時の更新(ELB のセキュリティグループからのアクセス許可を追加)は行われるものの、ELB の削除時の更新(追加されたアクセス許可を削除)が行われなくなります。

kubeconfig

EKS クラスタの作成後、kubectl で制御できるように ~/.kube/config を作成する必要があります。手作業で作成しなくても AWS CLI で下記のようにさっと作成できます。

aws eks update-kubeconfig --name hello-eks

Launch Configuration

EKS と直接関係はありませんが、ワーカーノードを Auto Scaling で作成できるようにするために Launch Configuration を作成する場合、使用している IAM User や IAM Role が下記のようにソースアドレスで制限されているとダメでした。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": [
            "192.0.2.123/32"
          ]
        }
      }
    }
  ]
}

Launch Configuration 以外にもいくつかソースアドレスで制限しているとダメな AWS リソースがあったと思います。

aws-auth の ConfigMap

ワーカーノードをクラスタと結合(普通はジョインと言うと思う)するためにはクラスタの作成後に Kubernetes に EC2 インスタンスプロファイルを認証するための ConfigMap を反映する必要があります。

aws-auth-cm.yaml をダウンロードしてインスタンスプロファイルの Role の ARN(Terraform の aws_iam_role.node.arn)を書き換えて反映します。

curl -O https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-08-30/aws-auth-cm.yaml
vim aws-auth-cm.yaml # rolearn  の部分を書き換え
kubectl apply -f aws-auth-cm.yaml

Kubernetes の Deployment や Service を作成

Service は ELB を使うので spec.typeLoadBalancer にします。

kubectl apply -f deploy.yaml

Service を作成すると ELB および ELB にアタッチされるセキュリティグループが自動的に作成され、さらに ELB からワーカーノードにアクセスできるようにするために、ワーカーノードのセキュリティグループに ELB のセキュリティグループからのアクセス許可が自動的に追加されます。

ELB の DNS 名は kubectl で確認できます。

kubectl get service httpd -o json | jq '.status.loadBalancer.ingress[].hostname' -r

サブネットの構成

クラスタの作成時に指定するサブネットで Public と Private を両方含めておくと、Kubernetes は自動的にサブネットの Public と Private を判断して Internet facing な ELB は Public に配置されます。

Private サブネットにはワーカーノードが配置されるわけですが、それは Kubernetes によって行われるわけではなく普通に EC2 インスタンスを実行したり AutoScaling を使うなりで配置するものなので、クラスタ作成時のサブネット指定は ELB が配置される Public サブネットだけで良いような気もしたのですが、ユーザーガイドを読んだ感じ Private サブネットも指定する必要があるようです。

クラスタ作成時に指定したサブネットには Kubernetes のコントロールプレーンと通信するための ENI が自動的に作成されるようなので、ワーカーノードが配置される Private サブネットも指定する必要がある、ということだと思います。

さいごに

EKS のユーザーガイドは下記にあります。

がしかし、これだけだと EKS の Kubernetes とは無関係な AWS の特有の事情しかわからないので、ちゃんと使うなら Kubernetes のドキュメントを読み進める必要があります。

クラスタを作成するだけでワーカーノードがなくても費用がかかるし、EC2 でワーカーノードを準備する必要があるので ECS と比べてお手軽感はありませんね。

また、ECS ならマネジメントコンソールで Task や Service の状態がいろいろ見れますが、EKS だとマネジメントコンソールではほとんどなにも見えなくて、基本的には kubectl を使う必要があります。ただ、どうせ CLI メインになるだろうので AWS CLI とか ECS CLI とかよりも kubectl で操作できる EKS の方が良いかもしれません。GUI が必要なら Kubernetes Dashboard というものも使えるようです。

EKS と直接は関係ない問題ですが、ワーカーノードを作成するために AutoScaling を使ってみましたが Terraform だと AutoScaling で AMI のアップデードに課題がありそうです。Rolling Update や Blue/Green Deployment は Terraform 単体できれいにやるのは難しそうです。

Amazon Elastic Container Service (ECS) を Terraform で素振り

ECS を Terraform で素振りしたメモ。Fargate 前提です。残骸はこちら

Fargate の費用

ECS は EC2 でクラスタを作るか Fargate かを選択できます。Fargate の方がマネージドなので楽ですが割高です。

ざっくりと EC2 の t3 インスタンスを比べてみます。Fargate の費用は以下のとおりですが。

AWS Fargate の料金

  • CPU → per vCPU per hour 0.0632USD
  • MEM → per GB per hour 0.0158USD

EC2 の t3 の費用と比較すると次のとおりです。

タイプ vCPU MEM USD/時間 Fargate
t3.nano 2 0.5 GiB 0.0068 0.1343
t3.micro 2 1 GiB 0.0136 0.1422
t3.small 2 2 GiB 0.0272 0.1580
t3.medium 2 4 GiB 0.0544 0.1896
t3.large 2 8 GiB 0.1088 0.2528
t3.xlarge 4 16 GiB 0.2176 0.5056
t3.2xlarge 8 32 GiB 0.4352 1.0112

Fargate がメモリに比べて CPU が高い?ため、小さめのインスタンスだと Fargate がかなり割高ですが、大きめのインスタンスだとだいたい2倍程度です。

ただし、EC2 はこれとは別に EBS の費用も必要です。Fargate のストレージはタスクあたり 10GB(コンテナあたり?)+ボリュームマウント用に 4GB と固定なものの、追加の費用はかからなさそうです。また、EC2 だと Rolling アップデートや Blue/Green デプロイしようとするとある程度の余剰(Blue/Green なら倍)リソースが必要になります(EC2 でもそのときだけスケールさせればいいのかもしれないけどデプロイがかなり辛くなりそう)。

なお、Fargate の最小構成は 0.25 vCPU / MEM 0.5 GB なので、最小でも .0237 (USD/時間) なので t3.nano と比べてだいぶ高いです。

クラスタとサービスとタスク

タスクは複数のコンテナによって構成されていて、事前に作成されたタスク定義をひな型として実行されます。ひとつのタスクの中の複数のコンテナは同じノードで実行されます。Fargate で実行するときのネットワーキングタイプの awsvpc だとタスクごとに ENI がアタッチされます。

サービスは↑のタスクが指定数が実行され続けるように維持したり、タスクのポートをELB のターゲットグループへ登録したりします。

クラスタはタスクを実行するためのコンピューティングリソースです。

ぐぐるともっとわかりやすい説明がたくさんあります。

タスクロールとタスク実行ロール

ECS でサービスやタスクを実行するとき、「タスクロール」と「タスク実行ロール」の2つのロールを設定します。

「タスク実行ロール」はタスクを実行するためのロールで(そのまんま)、 ECR からイメージを Pull したり、ログを CloudWatchLogs に記録するために使用されます。マネジメントコンソールで操作するのであれば基本的に自動で作成されるもので問題ありません。Terraform なら

「タスクロール」は実行されるコンテナに付与されるロールです。EC2 のインスタンスプロファイルみたいなものです。

新しいイメージのデプロイ

イメージを Dockerhub にプッシュしたあと、サービスの更新で「新しいデプロイの強制」を ON にして更新すると、新しいイメージがプルされてコンテナが開始され、古いコンテナが停止されます。AWS CLI でも相当のことはできます。

デフォルトだと Rolling update なので新旧のコンテナが混在します。試していないですが Blue/Green にもできます。

Terraform 単体だと・・・イメージをビルドして Dockerhub にプッシュするたびに、イメージのタグを変更し、tf ファイルのタスク定義を書き換えるしか無いですかね。あるいはタグは latest にして、デプロイには AWS CLI を使うとか。

タスクのスケジューリング

ECS のマネジメントコンソールに「タスクのスケジューリング」というのがあって、特定のタスク定義から Cron 風に定期的にタスクを実行させたりできます。ただし、これは実は CloudWatch Event のターゲットとして ECS タスクを指定しているだけなので、AWS CLI や Terraform で設定するときは CloudWatch Event の方を設定します。

プライベートサブネット

ECS サービスをプライベートサブネットに入れる場合、Docker イメージをプルしたりログを CloudWatch Logs に送信したりするために NAT ゲートウェイなり PrivateLink なりが必要です。 以下によると ECS からのイメージのプルには ECR の 2 つのエンドポイント以外に S3 も必要なようです。

ECS CLI

ECS のクラスタやタスクの作成や更新を行うための CLI ツールです。

ecs-cli up で VPC とサブネットを作成できます。が、サブネットをフロントとバックで分けたりセキュリティグループを細かく設定したりしようとすると ECS CLI だけでは完結できないので VPC などは Terraform で作っとけば良いように思います。

docker-compose.yml ファイルでタスクを定義をして、VPCやサブネットなどの ECS 固有のパラメータを ecs-params.yml で指定し、ecs-cli compose service up でサービスを開始できます。

ecs-cli compose service up \
    --cluster hello-ecs-cluster \
    --launch-type FARGATE \
    --create-log-groups \
    --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:999999999999:targetgroup/hello-ecs-http/9999999999999999 \
    --container-name app \
    --container-port 80

普段 docker-compose を使っていれば docker-compose.yml に慣れ親しんでいるので良いのですけど・・・ELB(ALB)との紐付けは ECS CLI のコマンドラインオプションで指定するしかない?

ecs-cli configure で IAM アクセスキーなどの認証情報やデフォルトのクラスタ・起動タイプ(Fargate/EC2)・リージョンなどを設定します。ただ、認証情報は AWS CLI のために設定した認証情報もそのまま使えるので ECS CLI 用に新たに設定する必要は無いと思います。デフォルトのクラスタや起動タイプも都度コマンドラインオプションで指定しても良いように思うので、ecs-cli configure しなくても良いような気もします。

うーん・・・ Terraform で十分な気がする? VPC やサブネットの ID を ecs-params.yml にベタ書きする必要があるし。

強いて言えば ECS CLI ならタスクの強制リスタート(新しいイメージをデプロイしたあとにそのイメージで起動し直す)とか、ecs-cli compose service ps コマンドでタスクの一覧をさっと見たり、ecs-cli compose run でタスクを one-shop で実行したり、ECR から pull/push も ECS CLI からできるので、Terraform と併用すると良いかも?

さいごに

マネジメントコンソールを触っていると、チュートリアル代わりなのだと思いますが「今すぐ始める」で CloudFormation でばこーんと一通りの環境を立ち上げてお試しすることができます。

また、「今すぐ始める」を使わなくても、クラスタを作成するときに一緒に VPC も作成できたり(これも CloudFormation だったと思う)、サービスを作成するときに一緒に ELB も作成できたり(これは CloudFormation ではなかったと思う)、至れり尽くせりなのですが、逆にどこでなにが作成されているかわかりにくいので、お試しで使う以外ではこれらの便利作成機能は使わなくて良いと思います。

Terraform で作成してみましたが、Fargate なら VPC(とそれに紐付くいろいろ)の作成が一番めんどくさくて、ECS 固有のものはタスク定義が Terraform の中にさらに JSON を書く必要があって微妙なの以外は難しいところはなさそうです。ただ EC2 でクラスタを組むのと比べると制限もあります。

Fargate だと Docker Volumes が使用できません。ので EFS をマウントして永続化ボリュームにしたりできません。Fargate では永続化ストレージには RDS とか S3 とサービスを使うしか無いようです。そもそも EFS のようなファイルシステムが必要という時点でなにかおかしいという意見もあると思いますが。

Fargate だとネットワーキングは awsvpc で固定です、host や bridge は使用できません。ただ awsvpc で十分な気もします。host や bridge でなければ困るようなユースケースあるかな? 強いて言えば awsvpc だとタスクごとに ENI が作成されるのでタスクをたくさん作ると ENI の上限にかかりやすいようです。

でもたいていのユースケースで Fargate で問題ないと思うし、スケールのために EC2 もオートスケールさせるのは Fargate と比べて面倒くさすぎるし、基本的に Fargate で良いと思う。

.

.

.

と思ったけど、オートスケールせずに固定的にリソース確保するのなら EC2 でクラスタ作るのでも良いかも。その場合 Blue/Green はまあ無理だけど、素のままでも Rolling Update できて Cron の冗長化も考えなくて良くなるなら、それだけでも素の EC2 と比べれば十分メリットはあるような気がする。

Fargate だとホストに SSH できないので、なにか問題があるときの調査がめちゃくちゃ困難だし。ただし、その場合でも EC2 インスタンスの可用性や手動スケーリングの容易さのために Auto Scaling Group は使っておいて良いと思う。

.

.

.

いやまあでも Fargate の制限は MySQL on EC2 に対する RDS for MySQL の制限みたいなものだと思えば Fargate 一択という気もする

virt-builder でサクッと作ったゲストが好みじゃなかった件

だいぶ前に Qiita で virt-builder でゲストを作って virt-resize でリサイズして virt-customize でカスタマイズ という記事を書いていて。

virt-builder ふむふむ便利そう、だがしかしなんか気に入らないので普通にゲスト作るときはやっぱ Kickstart だわ

と思ったはずなのだけど、なにが気に入らなかったのか忘れてしまったのでそのメモ。

virt-builder でサクッとゲストを作る

virt-builder という libvirt 管理下の KVM などの仮想環境にサクッとゲストを作るコマンドがあります。libguestfs-tools-c パッケージに含まれているのでインストールします。

yum -y install libguestfs-tools-c

まずはゲスト用のボリュームを作成します。

lvcreate vg1 -n vm.ore-no-virt -L 6G

ゲストのイメージの中身を弄るためのスクリプトを作ります。

cat <<'__RUN__'> run.sh
set -eux

# selinux
sed -i '/^SELINUX=/c SELINUX=disabled' /etc/selinux/config

# ngyuki
useradd ngyuki -m -g wheel

# authorized_keys
mkdir -p /home/ngyuki/.ssh
curl -fsSL https://github.com/ngyuki.keys | awk 1 > /home/ngyuki/.ssh/authorized_keys
chown -R ngyuki: /home/ngyuki/.ssh
chmod 700 /home/ngyuki/.ssh
chmod 600 /home/ngyuki/.ssh/authorized_keys

# sudoers wheel
tee /etc/sudoers.d/wheel <<EOS
%wheel ALL=(ALL) NOPASSWD: ALL
Defaults:%wheel env_keep += SSH_AUTH_SOCK
Defaults:%wheel !requiretty
Defaults:%root  !requiretty
EOS

# sudoers chmod
chmod 0440 /etc/sudoers.d/wheel

# ipv6
tee /etc/sysctl.d/ipv6-disable.conf <<EOS
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOS

# sshd
sed -i '/UseDNS /c UseDNS no' /etc/ssh/sshd_config
sed -i '/PermitRootLogin /c PermitRootLogin yes' /etc/ssh/sshd_config
sed -i '/AddressFamily /c AddressFamily inet' /etc/ssh/sshd_config

# postfix
postconf -e inet_protocols=ipv4
__RUN__

virt-builder でゲストのイメージを作ります。1回目はイメージのテンプレートがダウンロードされるので結構時間がかかります。2回目以降はキャッシュされているので早いです。

virt-builder centos-7.4 \
  --output /dev/vg1/vm.ore-no-virt \
  --arch x86_64 \
  --hostname ore-no-virt \
  --root-password disable \
  --timezone Asia/Tokyo \
  --run run.sh \
  --firstboot-command '
      nmcli con modify eth0 \
        connection.autoconnect yes \
        ipv4.method manual \
        ipv4.addresses 10.12.16.99/23 \
        ipv4.gateway 10.12.16.1 \
        ipv4.dns 10.12.16.2 \
        ipv6.method ignore
      nmcli con up eth0
    '

イメージを元に libvirt にインポートします。

virt-install \
  --name ore-no-virt \
  --hvm \
  --virt-type kvm \
  --ram 1024 \
  --vcpus 1 \
  --arch x86_64 \
  --os-type linux \
  --os-variant rhel7 \
  --boot hd \
  --disk path=/dev/vg1/vm.ore-no-virt \
  --network network=back \
  --graphics none \
  --serial pty \
  --console pty \
  --import \
  --noreboot

ゲストを開始して SSH でログインできます。

virsh start ore-no-virt
ssh ngyuki@10.12.16.99

気に入らなかった理由

イメージのテンプレートのパーティション構成がなんか好きではなかったので使ってなかったんでした。

sudo parted -l /dev/vda
#=> Model: Virtio Block Device (virtblk)
#=> Disk /dev/vda: 6442MB
#=> Sector size (logical/physical): 512B/512B
#=> Partition Table: msdos
#=> Disk Flags:
#=>
#=> Number  Start   End     Size    Type     File system     Flags
#=>  1      1049kB  1075MB  1074MB  primary  xfs             boot
#=>  2      1075MB  1720MB  645MB   primary  linux-swap(v1)
#=>  3      1720MB  6442MB  4723MB  primary  xfs

今日日仮想サーバなら /boot とか無くていいしスワップも必要なら別ディスクとしてアタッチするし、1つのディスクで複数パーティション切るぐらいなら複数のディスクをアタッチすればいいんじゃね?と思うので、仮想サーバでは基本的に1ディスク=1パーティションとしてます。

ので、この方法での環境構築はたぶんやりません。


この記事、元は 2018/03 ごろに書いてたものです。

MySQL で年月(yyyy/mm)のデータ型と PERIOD_ADD/DIFF

いわゆる請求データみたいなやつで請求年月のような yyyy/mm の値を表すために DATE 側を使うか整数型で yyyymm みたいにするか。

DATE 型で日付を 1 固定で持っていたとして、例えば請求年月が 2018/01 という条件で検索するとき 請求年月 = 2018/01/01 だと意味的におかしい気がする、請求年月 between 2018/01/01 and 2018/01/31 でなければならない。

そういう余計なことを考えなくていいようにするために yyyymm 形式の整数で持っておいて 請求年月 = 201801 とするのはアリだと思います。

ただ、例えば期間のデータで日割りが必要がないので yyyy/mm ~ yyyy/mm のように年月だけで持たせておくと、日付計算することも考えると整数型だと都合が悪いです。DATE型で持っていたほうが DATE_ADD とかがそのまま使えて便利です。

ただ、その場合 2018/01/01 ~ 2018/03/01 などとなるのは意味的におかしいので、2018/01/01 ~ 2018/03/31 とするべきだと感じます。

ただ、同じ、本当は業務的には年月なんだけど技術的な都合でDATE型にしている値でも、期間の FROM なのか TO なのかによって DB への格納方法が異なるのには違和感があります。

ただ、そもそも本当は業務的には「期間」というデータであってそれを技術的な都合でFROMとTOに分割しているのだと考えるとどっちてもいいんじゃないという気もする(どっちにしても業務的な意味とDBMSでの格納方法に齟齬がある)。


なお、2018/01/01 ~ 2018/03/31 のような形式で格納するようにすると、下記のような不安しか感じないコードを書いてしまうかもしれない。

select date_add(cast('2018/03/31' as date), interval 1 month)

なお、予想に反して?、期待したとおりの 2018/04/30 を返します。

なお、PHP の似たようなコードは予想通り?、期待に反して下記のような結果になります。

var_dump((new DateTime('2018/03/31'))->modify('+1 month')->format('Y/m/d'));
// string(10) "2018/05/01"
var_dump((new DateTime('2018/03/31'))->add(new DateInterval('P1M'))->format('Y/m/d'));
// string(10) "2018/05/01"

と思ってたらこんなのがあった。

SELECT PERIOD_ADD(200812, 1);
/* 200901 */

SELECT PERIOD_DIFF(200903, 200811);
/* 4 */

わざわざこんな関数が用意されているぐらいなので、MySQL で年月を表すデータ型は整数で良い?・・のかもしれない。

Doctrine や Eloquent や CakePHP はいかにして差分更新を実現しているか

Doctrine や Eloquent や CakePHP などの ORM でDBからフェッチしたエンティティの一部の属性だけ変更して保存したとき、テーブルの行全体が更新されるわけではなく、変更した一部の属性だけが更新されますが、それがどう実装されているか気になったので調べたメモ。

Eloquent

Laravel の Eloquent はエンティティ(モデル)が POPO ではないので、エンティティ自身にいろいろ情報が詰め込まれています。

モデルの HasAttributes トレイトで、

フェッチしたときの元の値を保持していて、

元の値との比較で更新すべき属性のリストを得ます。

CakePHP

Laravel の Eloquent と同じく、エンティティが POPO ではないのでエンティティ自身にいろいろ情報が詰め込まれています。

(実際に試してはいないんですけど)、EntityTrait トレイトで DB からフェッチしてきてから変更や追加されたプロパティの一覧を持っていて、

更新時に Entity から変化のあったプロパティだけ取り出して SQL を作ります。

Doctrine

Symfony などで使われる Doctrine はエンティティが POPO なので Eloquent や CakePHP のようにエンティティにいろいろ詰め込むことは出来ないはずですが?

EntityManager の中の UnitOfWork で、

$originalEntityData という、DBからフェッチした元の Entity の値を保持していて、

保存時に、フェッチしたときの元の値と Entity の値を比較して更新するセットを導出しています。

ので、 Entity 自体は POPO のままで、比較による差分での部分更新を実現していました。なかなか面白いですね、POPO なエンティティを扱う ORM は同じような実装になっているものなのでしょうか。

zend-db

zend-db の TableGateway や RowGateway は見た感じ差分更新のようなことは行われていなさそうです。RowGateway をフェッチして一部の属性を変更して save すると変更していない属性も含めて全部更新されそうです(試していない)。

さいごに

差分更新が出来ないと特定の状況ですごく不自然な動きになるように思います。

例えば、ユーザーというエンティティがあって、ユーザーの一部の属性だけ(氏名だけ、とか、メールアドレスだけ、とか)編集するフォームがあって、次のように処理していたとします。

  • Repository からユーザーのエンティティを取得
  • リクエストから値を取り出してエンティティの属性に反映
  • Repository でエンティティを DB に保存

ユーザーの氏名だけ変更数フォームと、メールアドレスだけ変更するフォームがあって、[A] は氏名のみを編集するリクエスト、[B] はメールアドレスを編集するリクエストです。この2つのリクエストが次のような順番で処理されると・・

  • [A] Repository からユーザーのエンティティを取得
  • [A] リクエストからメールアドレスを取り出してエンティティの属性に反映
  • [B] Repository からユーザーのエンティティを取得
  • [B] リクエストから氏名を取り出してエンティティの属性に反映
  • [A] Repository でエンティティを DB に保存
  • [B] Repository でエンティティを DB に保存

最後の段で差分更新が行われていないと [A] によるメールアドレスの変更は [B] による氏名の変更によって上書きされるので残りません。ですが [B] としては氏名だけ変更するフォームで操作しただけなので [A] に問い詰められても氏名しか変更してないので知らんがなです。

差分更新ができれば [A] によるメールアドレスの変更も [B] による氏名の変更も両方残ります。

DBからフェッチした時点で FOR UPDATE なロックしとけば大丈夫ですけど、これだけのために FOR UPDATE は過剰? でもないか??

あるいは、フォームにあわせた特定の属性だけ更新するメソッドをリポジトリに設けたり、

$this->userRepo->updateEmail($user->id, $user->email);

うーん、ログインユーザーの権限によって更新できる属性が異なる、などという仕様だったりすると「特定の属性だけ編集するフォーム」の「特定」が可変になるので破綻します。

では、更新する属性をリポジトリのメソッドで指定してみたり、

$this->userRepo->save($user, ['email']);

うーん、ありかな?

MRP(Meal RePlacement:食事代替品)を食してみたメモ

MRP(Meal RePlacement:食事代替品)をいくつか食してみたメモ。

自宅での食事ではこの類のものは食しておらず、職場で昼飯や晩飯を食べるときだけ食しています。また、昼飯は同僚何人かと弁当を注文することがあり、それが注文できるときには食していません(3人以上で注文しないといけないので希望者が少ないと注文できない)。晩飯は残業するときですが残業したくないです。

なので、この類のものを食す頻度はそれほど多くはありません。週に2~3回程度です。

コスパの比較

ざっくりとしたコストの比較。栄養とかはよくわからないので、カロリー量と1食あたりの価格のみ比較しています。価格はさっき見たときの価格、まとめ買いで安くなったりはするけれどもとりあえず最小購入単位、海外系は iHerb で買ってます。

カロリー量とは無関係に小分けされている1パック(COMP DRINK は半分)を1食としているので、1食あたりと1kcalあたりの価格に相関はありません。1食のカロリー量が少なすぎるような気がするのですが、食事のすべてを MRP に置き換えているわけではないので問題ないと思います。

品名 価格 カロリー 価格/食 価格/kcak
COMP POWDER 5,000 円 400 kcal x 12 417 円/食 1.04 円/kcak
COMP GUMI 5,000 円 400 kcal x 10 500 円/食 1.25 円/kcak
COMP DRINK 7,800 円 1000 kcal x 6 650 円/食 1.30 円/kcak
BASE PASTA quick 3,540 円 364 kcal x 6 590 円/食 1.62 円/kcak
Myoplex (EAS) 5,333 円 300 kcak x 20 265 円/食 0.88 円/kcak
RAW MEAL (Garden of Life) 2,712 円 240 kcak x 7 387 円/食 1.61 円/kcak

雑感

RAW MEAL(これが正式名なのかどうかよくわからない)はだいぶ昔に買ったのですが、Myoplex の方が飲みやすくてコスパも良かったので、1回買ったきりです。これだけはパックで小分けになっていないので備え付けのスコップ2杯で1食にしています。

Myoplex は同じ粉系でも RAW MEAL や COMP POWDER とかと比べて甘くて飲みやすいです。RAW MEAL はどうだったかもう忘れましたが COMP POWDER はなにかしら味をつけないと辛かったです。Myoplex はそのままでも OK でした。

COMP POWDER は素では飲めたものではなかったのでスムージーで味をつけて飲んでいました。ただいろいろなレビューを見ていると素でも大丈夫な人も居るようなので好みによるようです。

BASE PASTA quick は手軽だと聞いて試してみたのですが、飯!と思ってから食べるまでに電子レンジでチンする時間が必要なので、あまりお手軽感はありませんでした。レンジでチンするのと容器を振ったり洗ったりするののどちらが手間かは人によりけりだと思います。

COMP GUMI も手軽といえば手軽でしたけど、これを1食分を一気に食べるのはかなり辛いです。昼飯や晩飯の代替には向かなそうです。間食の感覚で少しずつ食べるものなのでしょうけど。

手軽さでは COMP DRINK が最強でした。開けて注いで飲むだけだし、2食目はパックから直でグビグビ飲めます。味も COMP POWDER を水に溶かしたものと比べれば飲みやすく、そのままグビグビいけます(最近 COMP POWDER を食していないので今はどうなのかわかりませんが)。

COMP DRINK のデメリットは、コスパがそんなに良くないのと、1パックで2食分なのに開封後はお早めにお召し上がる必要があるとこです。後述の通りこの類のものを食すのは不定期なので、計画的に2食食べる、というのがしにくいです。金曜日の夜とかに食すと翌週まで食す機会無いですし。500ml 版があればいいんですけどね。

まとめ

COMP DRINK が一番手軽で飲みやすく、メイン MRP (食事代替品)になってます。

ただ、前述の通り開封後の取扱が要注意なので、2回食す計画が練られないとき用に Myoplex も買っています。

COMP GUMI はなんか用途が違う気がします。BASE PASTA quick は自分にはあいませんでした。粉系は Myoplex が一番飲みやすくて良いです。