woshidan's blog

そんなことよりコードにダイブ。

CloudWatchにさわってみたときのメモ(カスタムメトリクスのパブリッシュ、アラームの設定)

とりあえず今日はCloudWatchの復習で。

CloudWatchはAWSのモニタリングや監視のためのサービスで、EC2など各種AWSのサービスを動かすと自動的にCPU利用率などのメトリクスを取得してくれる他、AWS CLIなどを利用してカスタムメトリクスを送ることも可能です。

CloudWatchの用語: 名前空間とディメンジョン、メトリクス

それぞれのメトリクスには名前空間とディメンジョン、名前があります。AWSサービスの場合の名前空間はサービス名であることが多いです。

f:id:woshidan:20180817022426p:plain

名前空間はそれぞれディメンジョンで区切ることができ、たとえばEC2名前空間に設定されているディメンジョンは

があります。(ディメンジョンが設定されていないデータについては

のように「範囲の定められていないメトリクス」に収められます。)

ディメンジョンをクリックすると出てくるCPU使用率などがメトリクスの名前にあたります。

カスタムメトリックを送信してみる

データのタイムスタンプと個別のデータポイント、あるいは、ある時刻のデータポイントの数、データの数値合計、平均といった集計値を送信することができます。 データのタイムスタンプは現在時刻から2週間前の間に指定する必要があります。

個別のデータポイントのデータを送る

TIMESTAMP=2018-08-08T08:08:00Z

aws cloudwatch put-metric-data --metric-name RequestLatency --namespace GetStarted \
--timestamp $TIMESTAMP --value 8 --unit Milliseconds

ある時刻のデータポイントの数、データの数値合計、平均といった集計値を送信する

TIMESTAMP=2018-08-08T08:08:00Z

aws cloudwatch put-metric-data --metric-name RequestLatency --namespace GetStarted \
--timestamp $TIMESTAMP --statistic-values Sum=40,Minimum=8,Maximum=8,SampleCount=8 --unit Milliseconds

送ったメトリクスは

f:id:woshidan:20180817023112p:plain

のようにグラフで確認可能です。

CloudWatchのアラームを作成する

CloudWatchのグラフを表示している画面から、CloudWatchのアラームを作成することができます。

アラームは、アラームの状態ごとにAmazon SNSと連携させることでSlackやメールで通知、PagerDutyなどインシデント管理サービスのアイテム作成の自動化に利用できます。

アラームの状態変化について

アラームは基本的にアラーム作成画面の

f:id:woshidan:20180817024410p:plain

の部分の条件式を満たしていたら OK、そうでない場合は ALARM , アラームの間隔内にデータポイントがない場合はINSUFFICIENT DATA の状態になります。

qiita.com

の記事にあるコマンドを

$ aws cloudwatch set-alarm-state --alarm-name "RequestLatency is High" --state-value ALARM --state-reason "alarm-test"

のように打ち込むと一瞬指定した状態にアラームの状態を変化させることができます。その後、すぐに直近のデータに応じた状態に戻ってしまいますが、アラームの通知連携のテストにはむしろ便利でしょう。

アラームの状態に応じて動作するプログラムなどをテストするときは、欠落データの処理方法を変更するとデータを送っていない間の状態が指定しやすくて便利です。

参考

今日は短いですが、現場からは以上です。

EC2インスタンスをECSの特定のクラスタに所属させる

今日は、EC2インスタンスをECSの特定のクラスタのコンテナインスタンスとして登録するために必要なものについてまとめておきます。

EC2インスタンスがECSのコンテナインスタンスになる仕組み

ECSのコンテナインスタンスecs-agentが動作していて、ecs-agentがECS APIを叩いてクラスタに登録したコンテナインスタンスなのですが、実は直接的にEC2インスタンスをECSのコンテナインスタンスとして登録するための設定はありません。

EC2インスタンスがECSのコンテナインスタンスとして動作するための条件を満たしていたら勝手にECSのクラスタにコンテナインスタンスとして登録される、という仕組みで初見は結構分かりにくかったです。。

EC2インスタンスがECSのコンテナインスタンスになるために必要なもの

  • ECSに最適化されたAMIの利用
  • AWSのアカウントに対する認証情報(どこの組織のECSインスタンスなのかわかる情報)が含まれ、ECSがコンテナインスタンスの登録やイメージの取得などに使うアクセス権限が付与されたIAMロール
  • IAMロールをEC2インスタンスにアタッチするためのインスタンスプロファイル
  • (EC2起動タイプを選択する場合のみ)SSHログインのためのキーペア
    • 基本的にECSに最適化されたLinux系のAMIを利用するインスタンスでは、パスワードによるSSHログインができないため*1
  • (強く推奨)コンテナインスタンスを起動するVPC
  • ecs-agentがECS APIと通信してコンテナインスタンスを登録するためのアウトバウンドの通信が許可されたセキュリティグループ
  • コンテナインスタンスを登録するクラスタを決定するためのuserdataで指定する環境変数 ECS_CLUSTERの指定
    • userdataに指定がなければデフォルトのクラスタに登録する

それぞれ、Terraformで具体的に指定する様子を確認して今日はおしまいにしようと思います。

必要なものを作成するためのTerraformの設定

ECSに最適化されたAMI

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-optimized_AMI.html などを参考に見繕う。

resource "aws_instance" "ecs_container_instance" {
  ami = "ami-e4657283" // 管理画面を操作しててでてきたのを使った気がする
  ...

IAMロール

このへんは昨日のエントリと同じですが。。

// 信頼ポリシーをロールに付与
resource "aws_iam_role" "ec2_instance_role" {
  name = "ec2_instance_role"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
    {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
    }
    ]
}
EOF
}
// アクセス権限ポリシー
resource "aws_iam_policy" "ecs_container_instance_policy" {
  name = "ecs_container_instance_policy"
  path = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:StartTelemetrySession",
        "ecs:UpdateContainerInstancesState",
        "ecs:Submit*",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}
// 信頼ポリシーが付与されているロールにさらにアクセス権限ポリシーを付与
resource "aws_iam_role_policy_attachment" "ecs_container_instance_policy_attachment" {
  role       = "${aws_iam_role.ec2_instance_role.name}"
  policy_arn = "${aws_iam_policy.ecs_container_instance_policy.arn}"
}

インスタンスプロファイル

// EC2インスタンスにはインスタンスプロファイルを利用してロールをアタッチ
resource "aws_iam_instance_profile" "ecs_container_instance_profile" {
  name = "ecs_container_instance_profile"
  role = "${aws_iam_role.ec2_instance_role.name}"
}
resource "aws_instance" "ecs_container_instance" {
  ...
  iam_instance_profile = "${aws_iam_instance_profile.ecs_container_instance_profile.name}"
}

キーペア

// public_key_pathで指定したパスに ssh-keygen で作成した公開鍵があるとして
resource "aws_key_pair" "auth" {
  key_name   = "${var.key_name}"
  public_key = "${file(var.public_key_path)}"
}

VPC

デフォルト*2のものを利用する場合、Terraformでの設定は不要。

セキュリティグループ

ECSインスタンスとして登録するだけなら、アウトバウンドの通信だけを許可すればよいです。実際はALBやAutoScalingGroupと組み合わせて利用するのでそれらのヘルスチェックや、連携して動く他のサービスからのリクエストを受け付けられるようにする必要がありますが。

resource "aws_security_group" "test_security_group" {
  name = "test_security_group"

  egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
  }
}

userdataで指定する環境変数 ECS_CLUSTERの指定

#!/bin/bash
echo ECS_CLUSTER=woshidan-test-cluster >> /etc/ecs/ecs.config

こういうスクリプト

$ cat userdata.sh | openssl enc -e -base64

Base64エンコードして

resource "aws_instance" "ecs_container_instance" {
  ...
  user_data = <<EOF
IyEvYmluL2Jhc2gKZWNobyBFQ1NfQ0xVU1RFUj13b3NoaWRhbi10ZXN0LWNsdXN0
ZXIgPj4gL2V0Yy9lY3MvZWNzLmNvbmZpZw==
EOF

のように指定します。

現場からは以上です。

参考

IAMのロール、EC2インスタンスやECSのタスクに対するロールについてメモ

今日はIAMのロールについて復習しようと思います。

TL;DR

  • 特別な作業をする時だけふだん使わない権限を一時的に使いたいときは、IAMロールの出番
  • IAMロールの使い方
    • IAMロール自体を作成し、
    • 誰がそのロールを利用できるのか、を記述した信頼ポリシー
    • 「~~をするためにxxが必要なんですが」)を記述したポリシーをアクセス権限ポリシー をロールにアタッチした上で
    • 許可されたユーザーやサービス(EC2, ECS, EMRなど)がAssumeRole APIにリクエストを投げて成功すると、一時的にロールに付与したアクセス権限ポリシーを持っているようにAPIを叩ける
  • IAMロールの使い所
    • 一時的に強い権限をユーザーに付与する、外部のユーザーに組織内のリソースへアクセス権限を与える、EC2インスタンス上のアプリケーションやECSのタスクに権限を付与する etc.
  • EC2インスタンス上のアプリケーションに権限を付与する場合はさらにIAMインスタンスプロファイルを用いてロールをインスタンスにアタッチする
  • ECSのタスクに権限を付与する場合は、作成したロールのARNをタスク定義のパラメータ(タスクロール: taskRoleArn)に指定する

IAMのロールに関する用語や使い方の確認

IAMはざっくり言うと、権限を管理する単位としてIAMユーザーというアカウントを用意して、IAMユーザーやIAMユーザーが所属するIAMグループ*1AWSのリソースへのアクセス権限を表すIAMポリシーをアタッチして権限を付与する、という感じのWebサービスです。

IAMユーザーというと聞きなれないですが、新しいチームに所属したときにAWSアカウントの管理者から「アカウント作成したよ」と渡される認証情報はIAMユーザーの認証情報ですね。

さて、この一度作成したAWSアカウントをしょっちゅう作り変える、という話も聞きませんし、コード管理しているとはいえしょっちゅうポリシーを変更して違う権限を付与するようにすると、「できると思っていたことができない」となって作業効率が落ちてしまいます。

なので、特別な作業をする時だけふだん使わない権限を一時的に使える仕組みがあると便利ですね。

そういうときは、IAMロールを作成して、IAMロールに特別な作業のための権限を記述したポリシーをアタッチします。

次に、誰がそのロールを利用できるのか、を記述した信頼ポリシーをたとえば、

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "sts:AssumeRole",
    "Principal": { "AWS": "arn:aws:iam::AWS-account-ID:user/user-name" },
    "Resource": "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:role/Test*"
  }
}

のように定義して*2作成したロールに付与すると、必要なときにAssumeRole APIを利用して*3そのロールを引き受ける*4ことで、一時的にふだん利用しているアカウントのものとは異なる権限が一時的に利用できるようになります。

また、「誰がそのロールを利用できるのか」を記述した信頼ポリシーに対して、一時的に付与したい実際やりたい作業のためのアクセス権限(「~~をするためにxxが必要なんですが」)を記述したポリシーをアクセス権限ポリシーと呼びます。

EC2インスタンス上で動くアプリケーションのための権限付与のためにIAMロールを利用する

このIAMロールは、EC2インスタンスの上でアプリケーションを動かす際の権限の付与にも利用できます*5

どういうことかというと、たとえば、IAMロールを利用しない場合、EC2インスタンス専用のIAMユーザーを用意して、EC2インスタンス上の設定ファイルにはそのユーザーの認証情報を書き込む、といった方法が考えられるんですが、認証情報を管理者以外がsshして見るかもしれないような場所におきたくないですよね。

IAMロールを利用して権限を付与する場合、EC2インスタンスの起動設定にアプリケーションで利用するロールを指定するだけでそのロールに付与されたポリシーに沿った権限がEC2インスタンスのアプリケーションから利用できるようになります。

ただし、EC2インスタンスがロールを利用する場合は、アクセス権限ポリシーの書き方はユーザーがロールを利用する場合と同じなのですが、信頼ポリシーの書き方がユーザーの場合と少し変わっていて、ロールを引き受けるリクエストを投げるのがEC2インスタンス=EC2のサービスだということで、

{
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "TrustPolicyStatementThatAllowsEC2ServiceToAssumeTheAttachedRole",
        "Effect": "Allow",
        "Principal": { "Service": "ec2.amazonaws.com" },
       "Action": "sts:AssumeRole"
    }
}   

のように、「誰がそのロールを利用できるのか」の「誰が」の部分を指定する Principal*6 の値が、

  • キーが "Service"
  • 値が "ec2.amazonaws.com"

となっています*7

EC2で何か具体的なアプリケーションの利用例、、ではないですが、具体例として

場合のTerraformの設定ファイルでも眺めてしめます。

具体例: EC2インスタンスをECSのコンテナインスタンスとして動かすための権限を付与する

refs:

// 信頼ポリシーをロールに付与
resource "aws_iam_role" "ec2_instance_role" {
  name = "ec2_instance_role"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
    {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
    }
    ]
}
EOF
}
// アクセス権限ポリシー
resource "aws_iam_policy" "ecs_container_instance_policy" {
  name = "ecs_container_instance_policy"
  path = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:StartTelemetrySession",
        "ecs:UpdateContainerInstancesState",
        "ecs:Submit*",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}
// 信頼ポリシーが付与されているロールにさらにアクセス権限ポリシーを付与
resource "aws_iam_role_policy_attachment" "ecs_container_instance_policy_attachment" {
  role       = "${aws_iam_role.ec2_instance_role.name}"
  policy_arn = "${aws_iam_policy.ecs_container_instance_policy.arn}"
}
// EC2インスタンスにはインスタンスプロファイルを利用してロールをアタッチ
resource "aws_iam_instance_profile" "ecs_container_instance_profile" {
  name = "ecs_container_instance_profile"
  role = "${aws_iam_role.ec2_instance_role.name}"
}
resource "aws_instance" "ecs_container_instance" {
  ...
  iam_instance_profile = "${aws_iam_instance_profile.ecs_container_instance_profile.name}"
}

具体例: ECS上で動くタスクにタスクロールを付与する

refs:

// タスクに付与する信頼ポリシーのプリンシパルは ecs-tasks.amazonaws.com
resource "aws_iam_role" "task_role" {
  name = "task_role"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
    {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "ecs-tasks.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
    }
    ]
}
EOF
}
// たとえばタスクにS3のListXxx APIへのリクエストを許可するアクセス権限ポリシーを書いてみる
resource "aws_iam_policy" "task_role_policy" {
  name = "task_role_policy"
  path = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:List*",
      ],
      "Resource": "*"
    }
  ]
}
EOF
}
// 信頼ポリシーが付与されているロールにさらにアクセス権限ポリシーを付与
resource "aws_iam_role_policy_attachment" "task_role_policy_attachment" {
  role       = "${aws_iam_role.task_role.name}"
  policy_arn = "${aws_iam_policy.task_role_policy.arn}"
}
// タスク定義を書く時 TaskRole として先ほど作成したロールを指定する
resource "aws_ecs_task_definition" "ecs_task" {
  family = "ecs_task"
  task_role_arn = "${aws_iam_role.task_role.arn}"
  container_definitions = <<DEFINITION
[
  {
    "essential": true,
    "image": "nginx:latest",
    "memoryReservation": 512,
    "name": "nginx"
  }
]
DEFINITION
}

思ったよりまとまらなかった...。現場からは以上です。

*1:IAMユーザーやIAMグループ, IAMロールなどの違いについてはこちら https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id.html

*2:詳細 https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_elements_principal.html

*3:AssumeRole APIを利用する場合の詳細は https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_switch-role-api.html 他。CLIの場合はsource_profileにロールを引き受ける権限があるプロファイル、role_arnに引きうけたいロールのARNを指定したプロファイルを作成する https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_switch-role-cli.html

*4:ロールをアタッチされて権限を切り替えることと同じくらいの意味で「ロールを引き受ける」という言葉を使うみたいです https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_terms-and-concepts.html など。なお、ロールを引き受けている間は、元のアクセス権限は利用できません

*5:外部アカウントのユーザーへのアクセス許可など、もっと他のユースケースについては https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_common-scenarios.html など

*6:IAMユーザー、IAMグループ、IAMロールといったIAMポリシーがアタッチされる対象をIAMの用語で「プリンシパル」といいます

*7:https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_use_passrole.html

Terraformで外部の設定ファイルから設定を読み込む(file, template_file, template_cloudinit_config)

今日はとりあえずTerraformのTemplateプロバイダーを少しさわってみようと思います*1

TerraformのTemplateプロバイダーには、データリソース*2として

  • template_file
  • template_cloudinit_config

リソースとして

  • template_dir*3

がありますが、今回は template_file を試してみます。

また、これらの前にファイル利用つながりで file 組み込み関数もためして、 template_cloudinit_config データリソースにも少しだけふれます。

TL;DR

  • 外部に書いた設定ファイルの内容を file 組み込み関数で読み出す
  • template_file データリソースを使って一部の設定を変数で置き換えられるようにする
  • EC2の起動時の設定で userdatacloud-init の両方を使う場合は template_cloudinit_config データリソースを使うと書きやすい

外部に書いた設定ファイルの内容を file 組み込み関数で読み出す

TerraformでAWSのリソースの設定を書いていると、JSONなどHCLで書きにくい部分があると思います。 そういった部分を外部ファイルに書き出して、 file 組み込み関数で必要な箇所で読みだすことができます。

たとえば、

resource "aws_iam_role" "test_role" {
  name = "test_role"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
    {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
    }
    ]
}
EOF
}

というtfファイルを書いた時、 <<EOF から EOF の部分を外部ファイルに抜き出して、

{
    "Version": "2012-10-17",
    "Statement": [
    {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
    }
    ]
}
resource "aws_iam_role" "test_role" {
  name = "test_role"
  assume_role_policy = "${file("./test_policy.json")}"
}

のように書くことができます。こうすることで、設定の方もシンタックスハイライトが効く環境で編集できるので捗りそうですね。

template_fileデータリソースを使って一部の設定を変数で置き換えられるようにする

設定は外部ファイルで書きたいのだけれど、設定の中で一部だけ変数を利用したい、という場合があります。 この場合は template_file データリソースを利用すると便利です。

たとえば、

resource "aws_iam_policy" "test_role_app" {
  name = "test_role_app"

  policy = "${file("./test_policy.json")}"
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:Describe*",
        "ecs:List*"
        ],
      "Resource": "arn:aws:ecs:region:account:cluster/my-cluster"
    }
  ]
}

のようなIAMポリシーがあったとして、対象のリソースだけを変えて同じようなアクションを許可するポリシーを他にも作りたい場合、template_file データリソースが使えます。

// ファイル名は test_policy.json から test_policy.json.tpl に変更
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:Describe*",
        "ecs:List*"
        ],
      "Resource": "arn:aws:ecs:region:account:cluster/${cluster_name}"
    }
  ]
}

のように外部設定ファイルを編集すると、 ${cluster_name} の部分を template_file データリソースのブロック中で設定できます。

data "template_file" "test_policy" {
  template = "${file("./test_policy.json.tpl")}"

  vars {
    cluster_name = "my_cluster"
  }
}

resource "aws_iam_policy" "test_role_app" {
  name = "test_role_app"

  policy = "${data.template_file.test_policy.rendered}"
}

EC2の起動時の設定でuserdataとcloud-initの両方を使う場合は template_cloudinit_config データリソースを使うと書きやすい

先ほどの template_file の設定は、EC2の起動時の設定で userdata を利用してECSのクラスターを指定する、といった設定の一部だけ置き換えたい、といった場合に便利そうですが、 ECSの設定は userdata だけでなく cloud-init を利用して行うことも可能です*4

そして、単純に userdatacloud-init のそれぞれの設定を書いた場合、両者の設定を共存させることはできません。

dev.classmethod.jp

userdatacloud-init の設定を共存させたい場合、両者の設定をマルチパート形式のテキストに変換し、userdata に書き込む必要があります。

これはなかなか大変ですが、 template_cloudinit_config データリソースの part 属性を利用するとこの変換作業を行う必要がなくなります。

www.terraform.io

現場からは以上です。

*1:今日からコマンドじゃなくてソフトウェアとしてのTerraformはTerraformと表記することにする

*2:プロバイダのページの見出し(たとえば、Templateプロバイダ https://www.terraform.io/docs/providers/template/index.html )ではData Sourcesと書いてあるが、だいたい日本語だとデータリソースと出てくる(たとえば、Terraform for さくらのクラウド データリソースとは https://sacloud.github.io/terraform-provider-sakuracloud/configuration/resources/data_resource/ )

*3:http://febc-yamamoto.hatenablog.jp/entry/2018/02/05/193735 の記事がTemplateプロバイダーのデータリソース、リソースについて詳しいです

*4:https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html#user-data-cloud-init

terraformのmoduleの基本的な使い方を試しました

あれもこれもやらなくては~と思っていたらどれにも手がついておらず結構ガッカリですが、ひとまず今日はterraformのmoduleの基本についてまとめておきます。

TL;DR

  • terraformがビルドに使うのはカレントディレクトリにある.tfファイル
  • moduleで他のディレクトリや他のサーバにある設定をコピーしてきて再利用する
    • moduleのsourceで指定するコピー元がローカルファイルでも terraform plan の前に terraform init 必要
  • variableを利用してmoduleを書いた側で一部の変数を代入する

terraformがビルドに使うのはカレントディレクトリにある.tfファイル

terraformが terraform planterraform apply した時に利用する設定ファイル(.tf)はterraformのコマンドを実行したディレクトリとなります。

たとえば、

$ tree .

.
├── foo
│   └── test_3.tf
├── test.tf
└── test_2.tf
// test.tf
provider "aws" {
    access_key = "HOGEHOGE"
    secret_key = "FUGAFUGA"
    region = "ap-northeast-1"
}
 
resource "aws_instance" "hoge" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"
}
// test_2.tf
resource "aws_instance" "fuga" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"
}
// test_3.tf
resource "aws_instance" "bar" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"
}

というようなディレクトリ構成のとき、カレントディレクトリで terraform plan すると結果に出てくるリソースは

$ terraform plan 
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.fuga
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...

  + aws_instance.hoge
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...

のように aws_instance.fuga, aws_instance.hoge の2つだけです。

他のディレクトリのファイルの設定を利用する場合は、次に説明する module を利用します。

moduleで他のディレクトリや他のサーバにある設定をコピーしてきて再利用する

moduleを使うと、

module "test" {
  source  = "./modules/test"
}

のように、参照元の設定ファイルがあるディレクトリを指定して、参照元に書かれた設定を他のディレクトリで利用することができます。

たとえば、先ほどの設定ファイルに対し、

// test.tf
provider "aws" {
    access_key = "HOGEHOGE"
    secret_key = "FUGAFUGA"
    region = "ap-northeast-1"
}
 
resource "aws_instance" "hoge" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"
}

module "foo" {
  source = "./foo"
}

のように module の部分を追加すると、terraform plan の結果に foo/test_3.tf に書かれていた aws_instance.barmodule.foo.aws_instance.bar として登場するようになります。

$ terraform plan 
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.fuga
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...

  + aws_instance.hoge
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...

  + module.foo.aws_instance.bar
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...

ここで、 test_2.tf にも同じ設定を追記すると

$ terraform plan 

Error: Failed to load root config module: Error loading modules: module foo: duplicated. module names must be unique

となります。

設定側で module に対して一意になるように名前をつけるのは、おそらく module が、どこかにある設定を利用する側の都合でコピーしてきたり、コピーしてきた設定に一部変数を代入して使うためのもののようだからと思います。

先ほどまで試していたローカルファイルシステム上の他のディレクトリは、 modulesource として指定できるデータの取得元の一部で、 https://www.terraform.io/docs/modules/sources.html によると、他には

  • Terraform Registry (Docker HubみたいにTerraformの設定をアップロードできるサイト)
  • GitHub
  • BitBucket
  • 他のgitリポジトリ
  • 特定のサイトのURL
  • S3のバケット

があります。

そういえば、この取得元の一覧を見ていると module で外部ソースを指定した時は git fetch ではないんですが、何かコマンドを打たなければいけないような気がします。

じつは、新しく module を設定に追加するとその module のソースがローカルファイルでも一度 terraform init を打つ必要があります。

terraform init を忘れて terraform plan を実行したときのログと、その後 terraform init を打って、./foo 以下から module foo の設定ファイルを取得している様子のログが以下となります。

$ terraform plan 

Error: Failed to load root config module: Error loading modules: module foo: not found, may need to run 'terraform init'

$ terraform init
Initializing modules...
- module.foo
  Getting source "./foo"

Initializing provider plugins...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

variableを利用してmoduleを書いた側で一部の変数を代入する

さて、moduleの使い方について「module がどこかにある設定を利用する側の都合でコピーしてきたり、コピーしてきた設定に一部変数を代入して使うためのもののようだ」と書きましたが、「コピーしてきた設定に一部変数を代入して使うためのもののようだ」の方についてももう少し詳しく見ておくことにします。

variables については今度 local と並べて使える型と使う場面の違いについてまとめようかな、と思いますが、terraformには

variable "hogehoge" {}

のように変数があることを宣言して、設定ファイル中で

${var.hogehoge}

のように利用できる変数 variables があります。各 variables の変数の値は、

  • 設定ファイル中で記載したり
  • 別の設定ファイルに抜き出してコマンドの実行時に -var-file=foo のように変数が書かれたファイルを指定したり
  • コマンドの実行時に -var 'foo=bar' のように一つずつ設定したり

できます。

設定ファイル中で記載できる箇所には module の中もあり、これを利用して

  • modulesource に指定する設定ファイル: 可変にしたい場所は ${var.env} のように variable を利用して書いておく
  • module を定義する側: module のブロックの中で、 source の設定ファイルに書かれた variable の値を設定する

のようにすると、production, staging環境で一部だけ設定が違う、というような場合の記述が便利にできます。

簡単なサンプルを書いてためしておきましょう。

$ tree .
.
├── modules
│   └── ec2.tf
├── production
│   └── main.tf
└── staging
    └── main.tf
// ./modules/ec2.tf
// stagingとproductionでAWSアカウントを分けてないとする
provider "aws" {
    access_key = "HOGEHOGE"
    secret_key = "FUGAFUGA"
    region = "ap-northeast-1"
}

variable "environment" {}
 
resource "aws_instance" "app" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"

    tags {
       Name = "app-${var.environment}"
       env  = "${var.environment}"
    }
}
// ./production/main.tf
module "app_production" {
    source = "./../modules"
    environment = "production"
}
// ./staing/main.tf
module "app_staging" {
    source = "./../modules"
    environment = "staging"
}
production$ terraform init
production$ terraform plan
...

  + module.app_production.aws_instance.app
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...
      instance_type:                "t2.nano"
      ...
      tags.Name:                    "app-production"
      tags.env:                     "production"
staging$ terraform init
staging$ terraform plan
...

  + module.app_staging.aws_instance.app
      id:                           <computed>
      ami:                          "ami-29160d47"
      arn:                          <computed>
      ...
      instance_type:                "t2.nano"
      ...
      tags.Name:                    "app-staging"
      tags.env:                     "staging"

現場からは以上です。

参考

Application Load Balancer(ALB)のコンポーネントについてメモ

先週、CLBではできなくてALBでできるようになったいくつかのことについてメモしました。

今週は、その記事に出てきたリスナーグループ、ターゲットグループといったALBの各コンポーネントに対し、そのコンポーネントを作成することで何を設定しているのかについてメモしておきます。

ロードバランサ

ALBのコンポーネントといったとき、それは実際のロードバランサのインスタンスであったり、管理画面上でぽちぽち追加していく一連の設定であったりすると思っていて、ロードバランサは

  • 一連の設定の中で一番大きい概念、かつ
  • 実際EC2インスタンスの上?*1で動いているだろうロードバランサ

のことを指します。

ALBのインスタンスを作成するときにはEC2の管理画面から作成しますが*2

  • 前提として利用するAZに少なくとも一つのパブリックサブネットを持つVPC

が必要です。ALBの作成時には名称以外に

などを指定します。さらに、作成時のフォームの流れで

も行います。

リスナー

ALBのリスナーは、

  • ALBが受信したリクエストのプロトコル、ポートをチェックして
  • リスナールールを用いてホスト、パスも見つつ、どのターゲットグループに転送したらよいかを決める

役割をするプロセスです。リスナーがチェックするプロトコルHTTPSの場合はリスナーに証明書をデプロイする必要があります。

リスナールール

リスナールールとは、

  • リスナーで指定したプロトコル・IPのリクエストがあった場合
  • ホスト・パスを見たり見なかったりして
  • どのターゲットグループへ転送するか
  • [HTTPSの場合] Amazon Cognito または OIDC を使用したユーザー認証を行うか

を決めるもので、リスナーに追加して利用します。

ALBで利用可能になったコンテントベースルーティング*6はリスナールールでのホスト・パスの条件設定と次の項のターゲットグループを用意して行います。

ターゲットグループ

CLBではロードバランサーに直接EC2インスタンスを追加し、追加したインスタンスの間でリクエストを分散させていましたが、ALBではリスナールールごとにリクエストの分散先を設定するようになりました。

このリスナールールごとに指定するリクエストの分散先の単位がターゲットグループです。ターゲットグループにEC2インスタンスをターゲットとして追加します。

ターゲットグループは配下のインスタンスにリクエストを転送してもよい状態かどうか確認するためのヘルスチェックの設定を持っています。

ターゲット

ロードバランサで外部からのリクエストを受け付け、リスナーがリスナールールに従ってリクエストを割り振るターゲットグループを決め、ターゲットグループの中で最終的にリクエストの転送先として選ばれるEC2インスタンスターゲットです。

instanceID(プライベートIPアドレスと紐付いている)か、プライベートIPアドレスを指定してターゲットグループに追加します*7

参考

現場からは以上です。

*1:マネージドサービスはどうもセキュリティグループなどの設定の構成を見ているとそうなっているきがする...

*2:https://cdn-ak.f.st-hatena.com/images/fotolife/w/woshidan/20180806/20180806093001.png

*3:セキュリティグループはEC2のインスタンスとしての設定なので、あとから詳細を修正・確認するときはEC2の管理画面から可能

*4:インターネット向けはロードバランサがリクエストを受信するまでの経路にインターネットを含む. このためロードバランサにパブリックDNSが作成時に割り振られる https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-internet-facing-load-balancers.html

*5:内部向けはロードバランサがリクエストを受信するまでの経路にインターネットを含まない。VPC内のあるインスタンスから別のインスタンス群にリクエストを分散させたい、といった場合に使うのか。VPC内部同士の通信であればプライベートIPアドレスで通信ができるので、作成時にパブリックDNS名は発行されない。パブリックDNSがなく外部からのアクセスの手段が提供されていないので内部向けロードバランサはVPC外部からのリクエストを受けつけない https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-internal-load-balancers.html

*6:ホスト名やパスを見てリクエストを割り振るインスタンス or インスタンスの集団を変える

*7:https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-type

ALBでできるようになったコンテントベースルーティングとALBと連携したECSの動的ポートマッピングについて

ELBとCLBとALBの違い

AWSのロードバランサで検索すると、ロードバランサを指す言葉に、ELB, CLB, ALBの3種類がでてきます。

この3種類の違いは、

  • ELB ... Elastic Load Balancing. サービス名。ELBの提供しているロードバランサのリソースにCLBとALBがある
  • CLB ... Classic Load Balancing. ELBで利用出来るロードバランサの種類。ALBの提供開始まではELBと呼ばれていて、サービス名とサービスで利用出来るリソースの名前が一致していた
  • ALB ... Application Load Balancing. ELBであとから利用出来るようになったロードバランサ。 コンテントベースルーティング やECSの動的ポートマッピングの利用が可能となった

となっています。以下は コンテントベースルーティング とECSの動的ポートマッピングについて、なぜALBではできるようになったのか、あるいは、どういうことが嬉しいのか、について調べて考えたことについてメモです。

ALBのコンテントベースルーティングについて

CLBを利用する場合、リクエスト元のプロトコルとポートを見て許可されたリクエストをロードバランサに追加されたEC2インスタンスに割り振ること以外できず、リクエストの内容を見て調整することはありませんでした*1

なので、1つのロードバランサに追加するEC2インスタンスは、基本的にすべて同一のインスタンスタイプ(というより同一の働きをする同一スペックのホスト)である必要がありました。

たとえば、Appサーバと画像配信用サーバを利用するサービス(ドメイン: sample.com )があり、

  • 基本的にはAppサーバへリクエストを転送する
  • /img/ のパスだけ画像配信用サーバへリクエストを転送する

のように同じドメインに対してリクエストを処理すべきサーバが複数種類ある場合、

  • まず sample.comドメインへ来たリクエストを1段目のCLBからすべてnginxに転送する
  • nginxがリクエストのパスを見て、パスが /img/ 以下なら2段目のCLB(画像配信サーバ用)、それ以外なら2段目のCLB(Appサーバ用)へリクエストを転送
  • 2段目のCLBが、複数のAppサーバ、画像配信サーバへリクエストを割り振る
  • リクエストが増えてきたら1段目のCLB配下のnginxを増やす

f:id:woshidan:20180803144804p:plain

といった構成になり、CLB以外にリクエストの転送先を切り替えるためにリーバスプロキシを挟む必要がありました*2

一方、ALBではロードバランサ自身がリクエストのパスを読んでリクエスト先を判断することができるので*3、リクエストの分散に対するネットワーク構成をシンプルにすることができます。

f:id:woshidan:20180803144816p:plain

ALBと連携することによるECSの動的ポートマッピングについて

ALBの登場により、ALBとサービスが連携することで一つのECSインスタンス上で同じタスクが複数実行できるようになり、ECSのサービスでタスクを複数実行するのが少し簡単になりました。

これについても少しメモしておきます。

CLBの頃には、

といった形でした。

つまり、ロードバランサから受信した際のプロトコル/ポートとバックエンドのEC2インスタンスへ転送する際のプロトコル/ポートが1対1で、転送先のポートは決めてから割り振るインスタンスを選ぶ感じだったんですね。

さて、タスクというのはだいたいdocker-composeで立ち上げるコンテナ一式のことなので、一つのECSインスタンスで同じタスク定義のタスクを複数立ち上げるということは、一つのDockerホスト上でポートマッピングに関する設定まで同じ定義のコンテナを複数立ち上げる、ということになりますが。。

いくらECSのDocker側でコンテナに対応するポートがたくさん公開されたとしても、ロードバランサーに来たプロトコル/ポートに対応してEC2インスタンスが受信するときのプロトコル/ポートは一つなので、そのうち一つのタスク(コンテナ. コンテナに対応するポート)にしかリクエストは転送されません*6

このため、CLBを利用して負荷分散をすることを念頭に置いた場合、ECSでは実質1インスタンス1タスクみたいな制限となっていたのでした。

ALBになって

という形になりました。

となりました。そうすると、CLBの頃にできなかった 同じインスタンス内の別のタスク(がリクエストを待ち受けているポート)へのリクエスト振り分け が可能となります。

余談

ここからは、あらためて、 https://aws.amazon.com/jp/premiumsupport/knowledge-center/dynamic-port-mapping-ecs/ のリンクを眺めた末の妄想に近いのですが、

たぶん、

  1. ロードバランサをセットしたサービスを作成する
  2. サービスがタスク起動時にタスクのコンテナが公開したポートを控えておく
  3. 2. で控えたポートとプロトコルを使ってタスク専用のターゲットグループを作成する
  4. 3. のターゲットグループに、タスクが動いてるEC2のインスタンスのIPにより自身が動作しているインスタンスをターゲットとして追加

というようなことをしているのだと思います。

ターゲットグループはロードバランサに対して作成するので、サービスは3, 4のステップのためにロードバランサの情報を知っておく必要があり、そのためにECSの動的ポートマッピングを利用する場合はサービス作成時にロードバランサを登録しているのでしょう。

現場からは以上です。

*1:他のサービス同様、ロードバランサのセキュリティグループやAClで、分散先のホストへ飛ぶ通信においてはこのポートはふさいでおこう、といった個別のリクエストの内容とは関係ないネットワークの経路に関する設定はできます

*2:https://aws.amazon.com/jp/elasticloadbalancing/pricing/

*3:あとで触れますが、ALBのリスナーのリスナールールによってパスごとに転送するターゲットグループを指定することができます https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-listeners.html#listener-rules 。 CLBのリスナーには同等の設定はありません https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/using-elb-listenerconfig-quickref.html

*4:具体的にはinstanceIDの指定により追加 https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-listener-config.html

*5:https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-listener-config.html CLBでリスナーを設定するフォームを確認してみてもそんな感じ https://cdn-ak.f.st-hatena.com/images/fotolife/w/woshidan/20180805/20180805202909.png

*6:リスナーとかいろんな設定を頑張ればいけるかもしれないが、たいがいなハックだと思います

*7:実際の設定画面が https://cdn-ak.f.st-hatena.com/images/fotolife/w/woshidan/20180805/20180805211910.png