[AWS][Terraform] Terraform で Amazon Inspector を導入する

TerraformAmazon Inspector を導入して、CloudWatch Events で定期実行させるための手順。
Terraform は v0.11.2 を使っています。

Inspector の導入

Inspector を導入するには、Assessment targets (評価ターゲット) と Assessment templates (評価テンプレート) を設定する必要があります。

Assessment targets の設定

Terraform で Assessment targets を設定するには、aws_inspector_resource_group, aws_inspector_assessment_target リソースを使用します。
こんな感じです。

inspector_target.tf
variable "project" { default = "my-big-project" }
variable "stage"   { default = "production" }

resource "aws_inspector_resource_group" "inspector" {
    tags {
        project   = "${var.project}"
        stage     = "${var.stage}"
        inspector = "true"
    }
}

resource "aws_inspector_assessment_target" "inspector" {
    name               = "my-inspector-target"
    resource_group_arn = "${aws_inspector_resource_group.inspector.arn}"
}

aws_inspector_resource_group では、対象となるインスタンスを特定するための条件を記述します。
上記の例だと、以下のタグが設定されているインスタンスを対象にします。

Name Value
project my-big-project
stage production
inspector true

aws_inspector_assessment_target では、aws_inspector_resource_group で定義した条件を元に Assessment targets を作成します。

Assessment templates の設定

Terraform で Assessment templates を設定するには、aws_inspector_assessment_template リソースを使用します。
こんな感じです。

inspector_template.tf
variable "inspector-rule" = {
    type = "list"
    default = [
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-7WNjqgGu",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-bBUQnxMq",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-gHP9oWNT",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-knGBhqEu"
    ]
}

resource "aws_inspector_assessment_template" "inspector" {
    name       = "my-inspector-template"
    target_arn = "${aws_inspector_assessment_target.inspector.arn}"
    duration   = 3600

    rules_package_arns = [ "${var.inspector-rule}" ]
}

output "assessment_template_arn" {
    value = "${aws_inspector_assessment_template.inspector.arn}"
}

rules_package_arns では、利用可能な Inspector rule package の ARN を設定します。
variable にしておくと、後で rule package を変更したい時に楽ですね。
こんな感じで、使用する rule package を変更できます。

terraform.tfvars
"inspector-rule" = [
    "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-7WNjqgGu",
    "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-bBUQnxMq"
]

使用できる rule package は、aws-cli で取得してください。

# パッケージ一覧の表示
$ aws --region ap-northeast-1 inspector list-rules-packages
{
    "rulesPackageArns": [
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-7WNjqgGu",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-bBUQnxMq",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-gHP9oWNT",
        "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-knGBhqEu"
    ]
}
# 詳細を確認
$ aws --region ap-northeast-1 inspector describe-rules-packages 
  --rules-package-arns "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-7WNjqgGu"
{
    "rulesPackages": [
        {
            "description": "The CIS Security Benchmarks program provides well-defined, un-biased and consensus-based industry best practicesto help organizations assess and improve their security.nnThe rules in this package help establish a secure configuration posture for the following operating systems:nn  -   Amazon Linux version 2015.03 (CIS benchmark v1.1.0)n  n    ",
            "version": "1.0",
            "name": "CIS Operating System Security Configuration Benchmarks",
            "arn": "arn:aws:inspector:ap-northeast-1:406045910587:rulespackage/0-7WNjqgGu",
            "provider": "Amazon Web Services, Inc."
        }
    ],
    "failedItems": {}
}

参考URL: Terraform v0.8.5でAWS Inspectorに対応します

これで terraform apply すれば Assessment targets, templates が作成されます。

動作確認

実際に Inspector が実施されるか確認して見ましょう。

$ aws inspector start-assessment-run 
  --assessment-template-arn arn:aws:inspector:ap-northeast-1:************:target/0-xxxxxxxx/template/0-xxxxxxxx
{
    "assessmentRunArn": "arn:aws:inspector:ap-northeast-1:************:target/0-xxxxxxxx/template/0-xxxxxxxx/run/0-7WNjqgGu"
}

実行状況の確認は aws inspector describe-assessment-runs

$ aws inspector describe-assessment-runs 
  --assessment-run-arns arn:aws:inspector:ap-northeast-1:************:target/0-QOvPswHA/template/0-uCIUy636/run/0-n9nnWOem

CloudWatch Events Schedule による定期実行

当初は CloudWatch Events で定期実行するには Lambda から呼び出すようにしなければいけませんでした。
しかし、CloudWatch Event から直接 Inspector を実行できるようになったため、Lambda を使用しなくても aws_cloudwatch_event_targetaws_cloudwatch_event_rule だけで定期実行設定が可能です。

CloudWatch Events で使用する IAM ロールの作成

まずは、CloudWatch Events で使用する IAM ロールを作ります。

cloudwatch-events-iam-role.tf
esource "aws_iam_role" "run_inspector_role" {
    name               = "cloudwatch-events-run-inspector-role"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_policy" "run_inspector_policy" {
    name        = "cloudwatch-events-run-inspector-policy"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "inspector:StartAssessmentRun"
            ],
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "run_inspector_role" {
    role       = "${aws_iam_role.run_inspector_role.name}"
    policy_arn = "${aws_iam_policy.run_inspector_policy.arn}"
}

CloudWatch Events への登録

CloudWatch Events に登録するために aws_cloudwatch_event_target リソースと aws_cloudwatch_event_rule を作りましょう。

cloudwatch-events.tf
variable "schedule"    { default = "cron(00 19 ? * Sun *)" }

resource "aws_cloudwatch_event_target" "inspector" {
  target_id = "inspector"
  rule      = "${aws_cloudwatch_event_rule.inspector.name}"
  arn       = "${aws_inspector_assessment_template.inspector.arn}"
  role_arn  = "${aws_iam_role.run_inspector_role.arn}"
}

resource "aws_cloudwatch_event_rule" "inspector" {
  name        = "run-inspector-event-rule"
  description = "Run Inspector"
  schedule_expression = "${var.schedule}"
}

schedule_expression の cron() は UTC で設定する必要があるので注意してください。
記述方法は、以下を参考に
参考URL: Rate または Cron を使用したスケジュール式

EC2 への IAM Role の設定と、ユーザーデータによる Inspector エージェントのインストール

評価ターゲットとなる EC2 には、Inspector エージェントがインストールされていて、適切なインスタンスロールが設定されている必要があります。
導入するには、こんな感じ

インスタンスロール

inspectora エージェントを使用するために必要なポリシーをアタッチしたインスタンスロールの作成はこんな感じです。

ec2-instance-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "my-ec2-role"
    path               = "/"
    assume_role_policy = <<POLICY
{
"Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_instance_profile" "instance_role" {
    name = "my-ec2-role"
    role = "${aws_iam_role.instance_role.name}"
}

resource "aws_iam_policy" "inspector" {
    name        = "my-ec2-iam-policy-inspector"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeCustomerGateways",
                "ec2:DescribeInstances",
                "ec2:DescribeTags",
                "ec2:DescribeInternetGateways",
                "ec2:DescribeNatGateways",
                "ec2:DescribeNetworkAcls",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribePrefixLists",
                "ec2:DescribeRegions",
                "ec2:DescribeRouteTables",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSubnets",
                "ec2:DescribeVpcEndpoints",
                "ec2:DescribeVpcPeeringConnections",
                "ec2:DescribeVpcs",
                "ec2:DescribeVpn",
                "ec2:DescribeVpnGateways",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeLoadBalancerAttributes",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth"
            ],
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "inspector" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.inspector.arn}"
}

ユーザーデータによる inspector エージェントのインストール

OS は Amazon Linux を想定してます。
ユーザーデータに書いておけば、インスタンス起動直後に inspector エージェントインストールできますね。
参考URL: Amazon Inspector エージェントをインストールする

ssh-key.pemssh-key.pem.pubssh-keygen で適当に作っておきましょう。

ec2.tf
## AMI
##
data "aws_ami" "amazonlinux" {
    most_recent = true
    owners      = ["amazon"]

    filter {
        name   = "architecture"
        values = ["x86_64"]
    }

    filter {
        name   = "root-device-type"
        values = ["ebs"]
    }

    filter {
        name   = "name"
        values = ["amzn-ami-hvm-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    filter {
        name   = "block-device-mapping.volume-type"
        values = ["gp2"]
    }
}

## SSH Key Pair
##
resource "aws_key_pair" "deployer" {
    key_name   = "ssh-key-name"
    public_key = "${file(ssh-key.pem.pub)}"
}

## EC2
##
resource "aws_instance" "ec2" {
    ami                         = "${data.aws_ami.amazonlinux.id}"
    instance_type               = "t2.micro"
    key_name                    = "${aws_key_pair.deployer.key_name}"
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.name}"

    user_data                   = <<USERDATA
#!/bin/bash
# install inspector agent
cd /tmp
/usr/bin/curl -O https://d1wk0tztpsntt1.cloudfront.net/linux/latest/install
/bin/bash install -u false
/bin/rm -f install
USERDATA

    tags {
        project   = "${var.project}"
        stage     = "${var.stage}"
        inspector = "true"
    }
}

現場からは以上です。

続きを読む

5分で構築、AmazonLinux+PHP7+Nginx+WordPress

使用した環境

以下環境のバージョンなど
AMIは、amzn-ami-hvm-2017.09.1.20171120-x86_64-gp2
Nginxは、version1.12.1
Wordpressは、version4.9.2日本語版

php70.x86_64                         7.0.25-1.26.amzn1             @amzn-updates
php70-cli.x86_64                     7.0.25-1.26.amzn1             @amzn-updates
php70-common.x86_64                  7.0.25-1.26.amzn1             @amzn-updates
php70-fpm.x86_64                     7.0.25-1.26.amzn1             @amzn-updates
php70-json.x86_64                    7.0.25-1.26.amzn1             @amzn-updates
php70-mbstring.x86_64                7.0.25-1.26.amzn1             @amzn-updates
php70-mysqlnd.x86_64                 7.0.25-1.26.amzn1             @amzn-updates
php70-pdo.x86_64                     7.0.25-1.26.amzn1             @amzn-updates
php70-process.x86_64                 7.0.25-1.26.amzn1             @amzn-updates
php70-xml.x86_64                     7.0.25-1.26.amzn1             @amzn-updates

インストール

yumのアップデート

sudo yum -y update

yumで必要なものを入れる

sudo yum -y install php70
sudo yum -y install php70-mbstring
sudo yum -y install php70-pdo
sudo yum -y install php70-fpm
sudo yum -y install php70-mysqlnd

設定

apacheのユーザから、nginxに置き換える

/etc/php-fpm.d/www.conf
user = nginx
group = nginx

Nginxの設定

nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    index   index.php index.html index.htm;

}

作成するサイトのドメインをsample.comとする

/etc/nginx/conf.d/sample.conf
server {
    listen       80;
    client_max_body_size 20M;
    server_name  sample.com;
    root         /var/www/html/sample;
    index        index.php index.html;

    location ~ .php$ {
        root           html;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME /var/www/html/sample$fastcgi_script_name;
        #fastcgi_param  PATH_INFO $fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_read_timeout 180;
    }


    include /etc/nginx/default.d/*.conf;

    location / {
    }

    # redirect server error pages to the static page /40x.html
    #
    error_page 404 /404.html;
        location = /40x.html {
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }

}

以下のコマンドを打って、/var/www/html以下に、wordpressを配置

cd /var/www/html
sudo wget https://ja.wordpress.org/wordpress-4.9.2-ja.zip
sudo unzip wordpress-4.9.2-ja.zip
sudo mv wordpress sample
sudo chown -R nginx:nginx sample

起動時にNginxとPHPが立ち上がるようにする

sudo chkconfig nginx on
sudo chkconfig php-fpm-7.0 on

起動

sudo service nginx start
sudo service php-fpm-7.0 start

アクセスしてWordpressをインストール

サーバのIPアドレスにアクセス

続きを読む

kube-awsで既存のvpc、subnetに入れるときにハマったのでメモ

kube-awsとは

10分でわかる「kube-aws」を参照ください。
kube-awsをメンテされている方にて記載されています。

既存のvpcに入れる時にハマった

kube-awsを使い始めて、既存のvpcになかなか入れられなかったのでメモ
試行錯誤した結果なので正しく無いかもしれないですが、このyamlで動きました。

cluster.yaml
clusterName: oreno-k8s
sshAccessAllowedSourceCIDRs:
- 0.0.0.0/0
apiEndpoints:
-
  name: default
  dnsName: oreno.k8s.dev.examlpe.com
  loadBalancer:
    subnets:
    - name: ExistingPublicSubnet1
    - name: ExistingPublicSubnet2
    hostedZone:
      id: ABCDEFGAAAAAA
keyName: oreno-keyname
region: us-west-2
kmsKeyArn: "arn:aws:kms:us-west-2:1234567890123:key/aaaaaaa-aaaaa-aaaaa-aaa-aaaaaaa"
controller:
  instanceType: t2.medium
  rootVolume:
    size: 30
    type: gp2
  autoScalingGroup:
    minSize: 2
    maxSize: 3
    rollingUpdateMinInstancesInService: 2
  subnets:
  - name: ExistingPublicSubnet1
  - name: ExistingPublicSubnet2
worker:
  nodePools:
    - name: nodepool1
      count: 2
      instanceType: t2.medium
      rootVolume:
        size: 30
        type: gp2
etcd:
  count: 3
  instanceType: t2.medium
  rootVolume:
    size: 30
    type: gp2
  subnets:
    - name: ExistingPublicSubnet1
    - name: ExistingPublicSubnet2
vpc:
  id: vpc-123456789
subnets:
  - name: ExistingPublicSubnet1
    availabilityZone: us-west-2a
    id: "subnet-1234567a"
  - name: ExistingPublicSubnet2
    availabilityZone: us-west-2c
    id: "subnet-9876543e"
containerRuntime: docker
kubernetesDashboard:
  adminPrivileges: true
  insecureLogin: false
addons:
  省略
experimental:
  省略

ポイント

vpc

vpcはvpc.id、vpc.routeTableId、vpc.vpcCIDRは既存VPCの値を入れる

vpc:
  id: vpc-123456789

subnets

subnetsのnameは任意、subnet.availabilityZone、subnet.idは既存のsubnetの値を入れる

subnets:
  - name: ExistingPublicSubnet1
    availabilityZone: us-west-2a
    id: "subnet-1234567a"
  - name: ExistingPublicSubnet2
    availabilityZone: us-west-2c
    id: "subnet-9876543e"

controller

controllerのsubnetsでsubnets.nameの値を指定する
複数指定することで複数AZで展開できる

controller:
  instanceType: t2.medium
  rootVolume:
    size: 30
    type: gp2
  autoScalingGroup:
    minSize: 2
    maxSize: 3
    rollingUpdateMinInstancesInService: 2
  subnets:
  - name: ExistingPublicSubnet1
  - name: ExistingPublicSubnet2

etcd

controllerと同じくsubnetsでsubnets.nameの値を指定する
複数指定することで複数AZで展開できる

etcd:
  count: 3
  instanceType: t2.medium
  rootVolume:
    size: 30
    type: gp2
  subnets:
    - name: ExistingPublicSubnet1
    - name: ExistingPublicSubnet2

worker

ここでハマったのですが、workerにもworker.nodePools.subnetsがありますが、subnetsを指定するとエラーが出てデプロイ出来なかったです。
subnetsを指定しないとデプロイが出来て、複数台を複数AZにデプロイしてくれました。
指定しなかったらデフォルトでvpc.subnetを見るのかな??教えてエラい人・・

worker:
  nodePools:
    - name: nodepool1
      count: 2
      instanceType: t2.medium
      rootVolume:
        size: 30
        type: gp2

PS.

kopsよりAWS寄りに実装されているので、AWSでk8sを扱う方はkube-awsはオススメです。

続きを読む

Spot Fleetを使ってEC2を1/4の料金で運用する

はじめに

普段EC2でサーバーを運用してるのですが、スポットインスタンスを使うことで約1/4の料金で利用できてます。

スクリーンショット 2018-01-08 13.40.58.png

スポットインスタンスについて

スクリーンショット 2018-01-08 13.11.12.png

  • 基本的にオンデマンドインスタンスに比べてかなり安いがオンデマンドインスタンスより値段が高くなる場合もある
  • 設定した最高価格の値段より高くなった時にデフォルトの動作だとインスタンスが削除される

停止にする設定もあります。

https://aws.amazon.com/jp/about-aws/whats-new/2017/09/amazon-ec2-spot-can-now-stop-and-start-your-spot-instances/

容量が指定料金内で利用不可能になりイベントが中断された場合に、Amazon EC2 スポットで Amazon EBS-backed インスタンスを終了する代わりに、それを停止することが可能になりました。

  • 同じインスタンスタイプでもavailability zoneで値段が違う
  • 上位のインスタンスタイプのほうが安くなる場合もある

最高価格

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-spot-instances-history.html

スポットインスタンスをリクエストするときは、デフォルトの上限料金 (オンデマンド料金) を使用することをお勧めします。

スポットインスタンスのインスタンスの価格がオンデマンドインスタンスの価格より高くなる場合があるので、オンデマンドインスタンスの上限に設定にしておいたほうがいいということだと思われます。

Spot Fleet

Spot Fleetを使うことで、予め最高価格と復数のインスタンスタイプとAvailability Zoneを設定しておくことでその中で一番安いスポットインスタンスをn個用意するということを自動で出来るようになります。

また動かしてるスポットインスタンスのインスタンスタイプの価格が高騰して停止した場合に、設定した他の安い条件のものがあった場合にそちらが新規で立ち上がるようになります。

Terraformを使ったECSで使う場合の設定例

user_data/ecs.sh.tpl
#!/bin/bash

echo ECS_CLUSTER=${ecs_cluster} >> /etc/ecs/ecs.config
aws_default_subnet.tf
resource "aws_default_subnet" "1a" {
  availability_zone = "${data.aws_region.current.name}a"
}

resource "aws_default_subnet" "1c" {
  availability_zone = "${data.aws_region.current.name}c"
}
aws_spot_fleet_request.tf
data "template_file" "aws_instance_app_user_data" {
  template = "${file("user_data/ecs.sh.tpl")}"

  vars {
    ecs_cluster = "app"
  }
}

data "aws_ami" "ecs_optimized" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-*-amazon-ecs-optimized"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
}

resource "aws_spot_fleet_request" "app" {
  iam_fleet_role  = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/aws-ec2-spot-fleet-tagging-role"
  spot_price      = "0.1290"
  target_capacity = "2"
  valid_until     = "2019-11-04T20:44:20Z"
  terminate_instances_with_expiration = true

  launch_specification {
    ami = "${data.aws_ami.ecs_optimized.id}"
    instance_type = "t2.large"
    iam_instance_profile = "${aws_iam_instance_profile.app.name}"
    vpc_security_group_ids = ["${aws_security_group.app.id}"]
    user_data       = "${data.template_file.aws_instance_app_user_data.rendered}"
    subnet_id       = "${aws_default_subnet.1a.id}"
    associate_public_ip_address = true

    tags {
      Name = "app"
    }
  }

  launch_specification {
    ami = "${data.aws_ami.ecs_optimized.id}"
    instance_type = "m4.large"
    iam_instance_profile = "${aws_iam_instance_profile.app.name}"
    vpc_security_group_ids = ["${aws_security_group.app.id}"]
    user_data       = "${data.template_file.aws_instance_app_user_data.rendered}"
    subnet_id       = "${aws_default_subnet.1a.id}"
    associate_public_ip_address = true

    tags {
      Name = "app"
    }
  }

  launch_specification {
    ami = "${data.aws_ami.ecs_optimized.id}"
    instance_type = "m4.large"
    iam_instance_profile = "${aws_iam_instance_profile.app.name}"
    vpc_security_group_ids = ["${aws_security_group.app.id}"]
    user_data       = "${data.template_file.aws_instance_app_user_data.rendered}"
    subnet_id       = "${aws_default_subnet.1c.id}"
    associate_public_ip_address = true

    tags {
      Name = "app"
    }
  } 
}

この設定の場合t2.largeのap-northeast-1a、m4.largeのap-northeast-1a, m4.largeのap-northeast-1cの中で一番安いもので、かつその料金が$0.1290以下の場合の時にEC2インスタンスを2つ動いてる状態にするという動作になります。
$0.1290はm4.largeのオンデマンドインスタンスの価格です。

またSpot Fleetの有効期限は2019-11-04T20:44:20Zで有効期限が切れた場合にインスタンスを停止するという設定になります。

最後に

一時的にサーバーが止まっても問題ない用途に使う場合は問題ないですが、ダウンタイムをないように運用するのはオンデマンドインスタンスと併用するなど工夫が必要です。

続きを読む

TerraformでWorkspaceを使わずに複数環境をDRYに設定する

TL;DR

Workspace機能を用いて複数環境を管理する代わりに、シンボリックリンクを駆使して共通設定を使い回します。

どうしてWorkspaceを使わないの? …という理由については、本記事の下の方に書いています。

サンプルリポジトリ

サンプルリポジトリを https://github.com/progrhyme/sample-terraform-symlink に作りました。
※動作確認はしていないので、何かありましたらプルリクエストなど下さい。

構成概要

ディレクトリ構成

.
├── dev/
│   ├── .envrc
│   ├── dev.auto.tfvars # dev環境の共通変数定義
│   ├── app/
│   │   ├── app-main.tf -> ../../shared/app-main.tf
│   │   ├── app-variables.tf -> ../../shared/app-variables.tf
│   │   ├── backend.tf
│   │   ├── common-variables.tf -> ../../shared/common-variables.tf
│   │   ├── dev.auto.tfvars -> ../dev.auto.tfvars
│   │   ├── global.auto.tfvars -> ../../shared/global.auto.tfvars
│   │   ├── main.tf
│   │   ├── provider.tf -> ../../shared/provider.tf
│   │   ├── terraform.tfvars
│   │   └── variables.tf
│   └── infra/
│       ├── backend.tf
│       ├── common-variables.tf -> ../../shared/common-variables.tf
│       ├── dev.auto.tfvars -> ../dev.auto.tfvars
│       ├── global.auto.tfvars -> ../../shared/global.auto.tfvars
│       ├── infra-main.tf -> ../../shared/infra-main.tf
│       ├── infra-outputs.tf -> ../../shared/infra-outputs.tf
│       ├── infra-variables.tf -> ../../shared/infra-variables.tf
│       ├── main.tf
│       ├── outputs.tf
│       ├── provider.tf -> ../../shared/provider.tf
│       ├── terraform.tfvars
│       └── variables.tf
├── modules/
│   ├── compute/
│   │   ├── main.tf
│   │   └── variables.tf
│   ├── db/
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── network/
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── prod/
│   ├── .envrc
│   ├── prod.auto.tfvars # prod環境の共通変数定義
│   ├── app/
│   │   ├── :
│   │   └── variables.tf
│   └── infra/
│       ├── :
│       └── variables.tf
└── shared/
    ├── app-main.tf         # appの共通レシピ
    ├── app-variables.tf    # appの共通変数設定
    ├── common-variables.tf # globalと環境ごとの共通変数設定
    ├── global.auto.tfvars  # 全環境・セグメントで共通の変数定義
    ├── infra-main.tf       # infraの共通レシピ
    ├── infra-outputs.tf    # infraの共通Output
    ├── infra-variables.tf  # infraの共通変数設定
    └── provider.tf

terraform実行例

cd dev/infra
terraform init # 初回のみ
terraform apply

このように dev/infraprod/app といったディレクトリがterraformを実行する場所になるわけですが、以降はこのterraform実行単位を「セグメント」と称することにします。

ポイント

  • トップ階層に dev/, prod/ といった環境単位のディレクトリを置いています。
  • dev/.envrc, prod/.envrcは、direnvを使って AWS_PROFILE を切り替えるためのものです。
  • 環境ごとに共通の変数定義は dev/dev.auto.tfvars, prod/prod.auto.tfvars にまとめています。
  • そのほか環境をまたいで共有するレシピは shared/ 配下に置きます。

メリット

  • 設定をDRYにできる。
  • どの変数を修正したらどの環境/セグメントに影響するかがわかりやすい。
  • direnvによって、環境ごとにクラウドの認証情報が異なる場合に、ディレクトリ移動だけで切り替えが可能。

課題

割とsymlinkが多く、複雑な構成になってしまったので、かえってわかりにくいという意見もあるかもしれません。

利用しているテクニック

Variable Filesの自動読込み

terraformを実行すると、カレントディレクトリの terraform.tfvars ファイルと *.auto.tfvars というsuffixのファイルが自動的に読み込まれます。

See https://www.terraform.io/docs/configuration/variables.html#variable-files

例えば、 dev/infra セグメントでは、 global.auto.tfvars, dev.auto.tfvars, terraform.tfvars の3つのVariables Filesをロードしています。

ちなみに、このファイル群が読み込まれる順序を把握していれば、最初に読み込まれるファイルでデフォルト値を定義し、後に読み込まれるファイルで上書きするという手法も使えますが、最終的にどの変数定義が使われるかがわかりにくくなるので、今はそういうことはしない方針にしています。

Remote Stateの参照

infraで構築したネットワークの情報を app から参照するようにしています。
以下に、サンプルリポジトリ中で関係するコードの一部を示します:

dev/app/backend.tf
data "terraform_remote_state" "infra" {
  backend = "s3"

  config {
    bucket = "<backet name>"
    key    = "dev-infra.tfstate"
    region = "<region>"
  }
}
shared/app-main.tf
module "compute" {
  source = "../../modules/compute"

  compute_bastion  = "${var.compute_bastion}"
  ec2_ssh_key_name = "${var.ec2_ssh_key_name}"

  network = "${data.terraform_remote_state.infra.network}" # ここで参照
}

参考:

VariableやOutputでmapを活用

上記の network 変数もそうですが、適宜mapを使うことで、variableやoutputを何度も記述する手間を減らしています。

例:

modules/network/outputs.tf
output "outputs" {
  value = "${map(
    "vpc_main_id", "${aws_vpc.main.id}",
    "security_group_main_default", "${aws_vpc.main.default_security_group_id}",
    "subnet_main_public1", "${aws_subnet.main_public.0.id}",
    "subnet_main_public2", "${aws_subnet.main_public.1.id}",
    "subnet_main_private1", "${aws_subnet.main_private.0.id}",
    "subnet_main_private2", "${aws_subnet.main_private.1.id}",
  )}"
}
shared/infra-variables.tf
variable "vpc_main"     { type = "map" }
variable "subnets_main" { type = "map" }
variable "db_main"      { type = "map" }
dev/infra/terraform.tfvars
vpc_main = {
  cidr = "10.0.0.0/16"
}

subnets_main = {
  public  = ["10.0.0.0/24", "10.0.1.0/24"]
  private = ["10.0.2.0/24", "10.0.3.0/24"]
}

db_main = {
  family            = "mysql5.7"
  engine            = "mysql"
  engine_version    = "5.7.19"
  instance_class    = "db.t2.micro"
  storage_type      = "gp2"
  allocated_storage = 20
  name              = "<db-name>"
  username          = "<db-username>"
  password          = "<db-password>"
}

参考:

Workspace機能について

Workspace機能を使うために

以上のような構成を前提とすると、環境間で構築するコンポーネントに差分がなく、WorkspaceごとにVariable Filesを上手く切り替える仕組みを整えれば、Workspace機能を使っても良さそうです。

「当該Workspaceでだけロードされるファイル」という仕組みがWorkspace機能の方で実装されたら、コンポーネント差異も吸収できるので、もっと使いやすくなりそうですね。

試してみてつらかったところ

Terraform Best Practices in 2017 – Qiitaのエントリを参考に一度、試してみたのですが、以下がつらみでした:

  • lookup地獄。書くのがつらかった。
  • Terraformのmapは要素に異なる型のものを入れられないといった制約があり、すべてをmapに押し込めるのが難しいと感じた。

まとめ

Terraformでsymlinkを使って設定を共通化する構成例を紹介しました。

たぶん、似たようなことをやっているプロジェクトは多いと思いますが、意外と記事など探してもあまり見つからなかったので、書いてみました。

どなたかの参考になれば幸いです。

続きを読む

ElasticIPを当てずにパブリックIPでドメインを半固定する

ElasticIPを当てずにパブリックIPでドメインを半固定する

はじめに

ElasticIPを当てるとお金かかるなぁ~。でもこの環境は起動時に名前当ててアクセスしたいなぁ~。って思っている節約家のための記事

やっていることとしては、インスタンス起動時スクリプトでroute53に自身のPublic IPを登録しにいくだけ。

対象読者はaws-cli少し読めてsystemdが少し分かる人だと良いと思う。
書いたコードまるっと載せてるので、同じ環境でそのままやったらできると思う。

環境

AWSのAmaon Linux 2のt2.nano

ソフトウェア バージョン等
AWS CLI 1.14.8
Amazon Linux2 Amazon Linux 2 LTS Candidate AMI 2017.12.0.20171212.2 x86_64 HVM GP2

動作に必要なパッケージ

  • aws-cli

    • 当然ながらRoute53にリクエストするために必要。Amazon Linux2ならデフォで入ってるはず
    • awsコマンド実行に必要EC2Roleが当たっている or aws configureは既に終わっている前提
  • jq
    • jsonをパースするために利用。

準備するファイル3つ

Hosted Zone IDを確認するためのスクリプト

1回使い切りのファイル。HostedZoneのIDが分かる場合は使わなくてOK。

check_hosted_zone_id.shって名前でも付けて任意の場所に配置しておいてください。

sh check_hosted_zone_id.shで実行するとドメイン名入力を要求するので、Route53に登録しているドメインを入力してください。存在する場合はIDが出ます。

#! /bin/sh
echo "Enter your domain(e.x. uji52.com)"
read DOMAIN
aws route53 list-hosted-zones|jq '.HostedZones[]|select(.Name=="'${DOMAIN}'.")|.Id'

AWS CLIを使ってRoute53に値を登録しにいくスクリプト

このスクリプトを起動時に呼び出してRoute53の値を更新に向かいます

2行目のIDは上のスクリプトで出力されたIDを貼り付けておいてください。

/usr/local/etc/route53/dns_update.shという名前で配置してchmod +xで実行可能にしておいてください。

.oO(もっと良い置き場所ある気がしている)

#! /bin/sh
HOSTED_ZONE_ID="/hostedzone/ZXXXXXX525252"
IP_ADDR=`curl 169.254.169.254/latest/meta-data/public-ipv4 2> /dev/null`
DATE=`date`
sed "s/<%DATE%>/${DATE}/" < /usr/local/etc/route53/route53.json.template | sed "s/<%IP_ADDR%>/${IP_ADDR}/" > /usr/local/etc/route53/route53.json
aws route53 change-resource-record-sets --hosted-zone-id ${HOSTED_ZONE_ID} --change-batch file:///usr/local/etc/route53/route53.json

Route53にリクエストする為のjsonファイルのテンプレート

Route53は古くからのサービス故、CLIでもjsonファイルを送りこまなきゃ設定ができない子なので、送り込むためのjsonファイルをこんな感じで/usr/local/etc/route53/route53.json.templateという名前で配置。

Nameのところに、このインスタンスに付けたい名前を付けておきましょう。
UPSERTなので、なかったら作ってくれるし、あったら更新してくれるよ

{
  "Comment": "<%DATE%>",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "test.uji52.com.",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "<%IP_ADDR%>"
          }
        ]
      }
    }
  ]
}

実際に更新してみる

$ /usr/local/etc/route53/dns_update.sh
{
    "ChangeInfo": {
        "Status": "PENDING", 
        "Comment": "Sun Dec 24 06:45:45 UTC 2017", 
        "SubmittedAt": "2017-12-24T06:45:46.115Z", 
        "Id": "/change/ZXXXXXX525252"
    }
}

上記のようなログが出力されれば更新が完了しているので、実際にAWSマネジメントコンソールから確認してみると変更が確認できます。

自動起動にしてみる

正直systemdに詳しくないので自信は無いけどこんな感じで記述

ファイル名は/etc/systemd/system/route53.service

[Unit]
Description=DNS Update
After=network.target

[Service]
Type=simple
RemainAfterExit=yes
ExecStart=/opt/route53/dns_update.sh

[Install]
WantedBy=multi-user.target

設定作ったらデーモンをリロードせあかんらしいから再起動してサービス起動して確認。

$ systemctl daemon-reload
$ systemctl start route53
$ systemctl status route53
● route53.service - DNS Update
   Loaded: loaded (/etc/systemd/system/route53.service; enabled; vendor preset: disabled)
   Active: active (exited) since Sun 2017-12-24 06:10:24 UTC; 4s ago
  Process: 3534 ExecStart=/usr/local/etc/route53/dns_update.sh (code=exited, status=0/SUCCESS)
 Main PID: 3534 (code=exited, status=0/SUCCESS)

Dec 24 06:10:24 xxx systemd[1]: Started DNS Update.
Dec 24 06:10:24 xxx systemd[1]: Starting DNS Update...
Dec 24 06:10:26 xxx dns_update.sh[3534]: {
Dec 24 06:10:26 xxx dns_update.sh[3534]: "ChangeInfo": {
Dec 24 06:10:26 xxx dns_update.sh[3534]: "Status": "PENDING",
Dec 24 06:10:26 xxx dns_update.sh[3534]: "Comment": "Sun Dec 24 06:10:24...,
Dec 24 06:10:26 xxx dns_update.sh[3534]: "SubmittedAt": "2017-12-24T06:1...,
Dec 24 06:10:26 xxx dns_update.sh[3534]: "Id": "/change/ZXXXXXX525252"
Dec 24 06:10:26 xxx dns_update.sh[3534]: }
Dec 24 06:10:26 xxx dns_update.sh[3534]: }
Hint: Some lines were ellipsized, use -l to show in full.

更新ログが良い感じに出てるから、systemdで呼び出しはしっかりできてるっぽい。

ので、自動起動として登録して完了

$ systemctl enable route53

まとめ

すぐできるし作ろうと思うも、めんどくさくて結局やらんかったんです。
まぁ節約意識大事やと思うんで、日曜大工感覚で作成。

ざっくりしたメモとして残しているだけなので、手順に不足等あったらお教えくださいませ。

続きを読む

ある程度の規模で運用するAWS CloudFormationの勘所

概要

インフラエンジニアとしてAWS基盤の構築・運用に携わって早1年が経ちました。
今回は自分がCloudFormationを運用する中で培ってきたノウハウや勘所をご紹介したいと思います。

なお、これがCloudFormationのベストプラクティスだとかそんなことを言うつもりはなく、
あくまで自分がこう考えてきたぞというものなので、ご参考程度にお願いします。
いろんな考え方があると思いますので、ぜひマサカリコメントお待ちしてます。

どの程度の規模で運用してきたか?

サービスとしてはビッグデータ分析プラットフォームのようなものを構築しておりますが、
AWSの規模感としては大体こんな感じです。

  • AWSアカウント:2

    • 1つは開発環境・内部結合環境用
    • 1つはステージング環境、本番環境用
  • 環境数:5
    • 開発環境
    • 内部結合環境
    • ステージング環境1
    • ステージング環境2
    • 本番環境
  • 利用しているAWSサービス:約15サービス
    • Amazon VPC
    • Amazon EC2
    • Amazon RDS
    • Amazon ElastiCache
    • Amazon S3
    • Amazon DynamoDB
    • Amazon CloudWatch
    • Amazon SNS
    • Amazon Cognito
    • Amazon Route53
    • AWS Lambda
    • AWS IAM
    • Amazon Kinesis
    • AWS WAF
    • AWS CloudTrail
  • 基盤担当者:2~3名
  • CloudFormation テンプレートステップ数:約80KStep

CloudFormationの適用範囲

さてCloudFormationを利用するにあたって、どのAWSリソースをCloudFormationで管理すべきでしょうか?
個人的には「可能な限り全て」を推奨しています。
「可能な限り全て」というのは、「EC2のキーペア登録などCloudFormationでは管理できないもの、新規サービス等でCloudFormationが対応していないものを除き、CloudFormationで構築可能なAWSリソースの全て」という意味です。

AWSの利用サービスが多いほど全てに対応するのは大変に思えるかもしれません。
しかし管理方法(AWS CLI, CloudFormation, 管理コンソールなど)がバラバラになるよりかは遥かに混乱せずミスも防げます。
特にCloudFormationで作成したリソースをCloudFormation以外で更新・削除してしまうと整合性が取れなくなり、最悪CloudFormationの運用ができなくなってしまうので、そのような事故を避けるという意味でも原則CloudFormationに統一することをお勧めします。

1つの技術要素に統一しておけばキャッチアップコストも低くなるでしょう。

テンプレートフォーマット

テンプレートのフォーマットはJSONとYAMLが選択できますが、これは可読性の観点から「YAML一択」です。
もともとJSONのみのサポートでコメントが書けない等の問題がありましたが、2016年9月のアップデート1でYAMLがサポートされるようになりました。

既にJSONフォーマットのテンプレートを利用している場合でも、CloudFormationデザイナーを利用してコンバート可能なので積極的にYAMLフォーマットを利用しましょう。

ディレクトリ/ファイル構成

ある程度の規模のAWSリソースを管理することが想定される場合、事前にディレクトリ構成やファイル構成をしっかり考えておかないと管理が非常につらくなってきます。

特にファイル構成(1テンプレートファイルに何のAWSリソースを含めるか)は、1度スタックを作成してしまうと後から容易に変更ができないため重要です。

ディレクトリ構成

適切なファイルの構成を考えるためには、適切なディレクトリ構成を考える必要があります。
開発者や運用者がテンプレートファイルを管理しやすい構成が望ましいでしょう。

筆者のチームでは「AWS契約単位」、「環境単位」、「システム or サブシステム単位」にディレクトリを分割することを推奨 2しています。

ディレクトリ構成例
cloudformation
├─ aws-000000000000              # AWSアカウントID[000000000000]のリソースのテンプレートを格納
│  ├─ common                     # 環境共通的なリソースのテンプレートを格納(IAM設定, CloudTrail設定など)
│  │  ├─ iam.template
│  │  ├─ cloudtrail.template
│  │  ├─ …
│  │
│  ├─ production                 # 本番環境のリソースのテンプレートを格納
│  │  ├─ common                  # 本番環境のシステム/サブシステム共通的なリソースのテンプレートを格納  
│  │  │  ├─ network.template
│  │  │  ├─ s3.template
│  │  │  ├─ dns.template
│  │  │  ├─ …
│  │  │
│  │  ├─ systemA                 # 本番環境のAシステム/サブシステムのリソースの/テンプレートを格納
│  │  │  ├─ composite.template
│  │  │  ├─ …
│  │  │
│  │  ├─ systemB                 # 本番環境のBシステム/サブシステムのリソースの/テンプレートを格納
│  │  │  ├─ …
│  │  │
│  │  ├─ …
│  │
│  ├─ staging1                   # ステージング1環境のテンプレートを格納
│  │  ├─ …
│  │
│  └─ staging2                   # ステージング2環境のテンプレートを格納
│     ├─ …
│
└─ aws-111111111111              # AWSアカウントID[111111111111]のリソースのテンプレートを格納
   ├─ …

ファイル構成

ディレクトリ構成が決まるとファイルの構成が概ね見えてきます。
上記のディレクトリ構成に基づくと、1つのテンプレートファイルに複数環境のAWSリソースが存在したり、複数システムのAWSリソースが存在したりするということはあり得ません。

仮に1テンプレートファイルに本番とステージング環境のAWSリソースが混在する場合を考えてみましょう。
ステージング環境のAWSリソースを更新する際は、必然的に本番環境のAWSリソースを含むスタックを更新することになります。仮に本番環境のAWSリソースに変更を加えていないとしても、精神衛生上よろしいものではありませんね。
事故を未然に防ぐという意味でも、最低限「環境単位」、「システム or サブシステム単位」にディレクトリを分割することは有効です。

ではディレクトリ内のファイル単位についてはどう考えるべきでしょうか。
ここで考慮すべきはAWSリソース間の依存度AWS管理者の単位です。

AWSリソース間の依存度

  • 互いに依存度の高いAWSリソースは同一テンプレートで管理すべきです。
    別テンプレートで管理してしまうと、AWSリソース間の依存関係を人が意識してスタックの作成・更新・削除を行わなければなりません。これはAWSに熟練した人ならまだしも、通常は容易なことではありません。
     
  • 互いに依存度の低いAWSリソースはテンプレートで管理すべきです。
    そうすることでスタックの作成・更新・削除時の影響を極小化することができます。(他のリソースをうっかり更新して事故を起こす可能性がなくなります。)

AWS管理者の単位

  • 例えばアカウント(IAM)管理者、データベース(RDS)管理者といったようにAWSのリソースに対して管理者が分かれている場合は、権限制御の観点から1テンプレートファイルに含めるリソースを判断したほうがよいでしょう。

筆者の経験上、上記のディレクトリ構成に基づくのであれば、ディレクトリ配下のAWSリソースは1テンプレートファイルにまとめてしまったほうが運用しやすいです。
実際system系のディレクトリ配下は各種AWSリソース(ALB, EC2, RDS, SecurityGroup, IAMRole, InstanceProfile etc..)をcomposite.templateにひとまとめにしており、分割しているのはcommonディレクトリに含まれるIAM(ユーザ・グループ)、CloudTrailなど明確に他のAWSリソースとの結合度が低いもののみとなっております。

テンプレートの共通化

ここまで読んでいただいた方は「テンプレートの共通化をしないのか?」と思われるかもしれません。

公式ドキュメントのAWS CloudFormationのベストプラクティスでも紹介されていますが、テンプレートファイルはパラメータを利用することによって共通化することが可能です。
同じコンポーネントを宣言する共通パターンを共通テンプレートとして再利用することで、ダブルメンテを防ぐことができます。

しかしながら、筆者はテンプレートの共通化は極力しないほうがむしろメンテナンス性は高いと考えています。

1. 可読性が低い

共通化をしようとすればするほど、各種リソースの設定値をパラメータ化して、外部から値を受け取るようになります。極端な例ですがEC2インスタンスを作成するテンプレートを共通化すると下記のようになります。
パラメータを利用しているため、具体的に何の値が設定されているかは管理コンソールや呼び出し元の親テンプレートを参照しないとわかりません。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  ImageId:
    Description: EC2 ImageId
    Type: String
    Default: ""
  InstanceType:
    Description: EC2 InstanceType
    Type: String
    Default: ""
  AvailabilityZone:
    Description: EC2 AvailabilityZone
    Type: String
    Default: ""
  InstanceInitiatedShutdownBehavior:
    Description: EC2 InstanceInitiatedShutdownBehavior
    Type: String
    Default: ""
  DeviceName:
    Description: EC2 DeviceName
    Type: String
    Default: ""
  VolumeType:
    Description: EC2 VolumeType
    Type: String
    Default: ""
  VolumeSize:
    Description: EC2 VolumeSize
    Type: String
    Default: ""

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      AvailabilityZone: !Ref AvailabilityZone
      InstanceInitiatedShutdownBehavior: !Ref InstanceInitiatedShutdownBehavior
      BlockDeviceMappings:
        - DeviceName: !Ref DeviceName
          Ebs:
            VolumeType: !Ref VolumeType
            VolumeSize: !Ref VolumeSize
      # 省略

共通化をしない場合、下記のようにパラメータを利用せず設定値をベタ書きする形になります。
似たような記述を繰り返し書くことになりますが、実際のリソースとテンプレートの定義が1対1で定義されており、設定値が一目でわかります。

AWSTemplateFormatVersion: 2010-09-09
Resources:
  EC2Instance001:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-da9e2cbc
      InstanceType: t2.micro
      AvailabilityZone: ap-northeast-1a
      InstanceInitiatedShutdownBehavior: stop
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: 100
            VolumeSize: gp2
      # 省略

  EC2Instance002:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-da9e2cbc
      InstanceType: t2.large
      AvailabilityZone: ap-northeast-1c
      InstanceInitiatedShutdownBehavior: stop
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: 200
            VolumeSize: gp2
      # 省略

2. 修正に伴う影響範囲が大きくなる

共通化できると考えていたとしても後から「この環境だけ、もしくはこのリソースだけ個別に変更を加えたい」というようなことは往々にして発生します。
共通テンプレートを修正する場合は影響を受けるリソースを常に意識しなければなりません。
最悪、共通化したテンプレートの中でConditionによる条件分岐を行うなど、負の遺産を生み出しかねません。

3. 変更プレビューにてネストされたスタックの変更分が参照できない

これは親テンプレートを用意してその中で共通テンプレートを利用する場合に発生する問題となります。
管理コンソールからCloudFormationを実行する場合、実行前に「変更のプレビュー」として、差分を確認できますが、親スタック(テンプレート)の更新を行う場合、子スタック(テンプレート)の詳細な変更分は見ることができません。

Changes.png

仮にテンプレートファイルの差分を事前に別の方法で確認していたとしても、これは精神衛生上非常によくありませんし、思わぬ事故を引き起こすかもしれません。
 
 
以上の理由から、テンプレートファイルはプログラマブルに共通化することによって返って複雑度が増してしまうと考えています。
筆者のチームでは、原則共通化禁止ネストスタック禁止という形で可能な限りテンプレートをわかりやすくシンプルにしてきました。
単純、故に冗長な部分もありますが、裏を返せば簡単であり、知識の少ない運用者でもキャッチアップが容易でミスも最小化できます。

コーディング規約

コーディング規約というほど大それたものではありませんが、テンプレートを作成するにあたっていくつか決めておいたほうがいいことがあります。

  • AWSリソースのキー名
    下記でいうEC2InstanceProductionSystemA0001に相当する部分となります。
    筆者のチームでは「AWSリソース名」+「環境名」+「システム名」+「連番」としています。
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  EC2InstanceProductionSystemA0001:
    Type: AWS::EC2::Instance
    # 以下省略
  • AWSリソースに付与するタグ
    筆者のチームでは「環境」、「システム名」、「一意の名称」は最低限必須としています。
     
  • 使用しないプロパティの記載要否
    使用しないプロパティの記載方法については次の3パターンが考えられます。
    筆者のチームでは明示的に使用しない意図が分かるよう「3. 記載した上でAWS::NoValueを参照する」方針としています。

    1. 使用しないプロパティは記載しない
    2. 記載した上でコメントアウトする
    3. 記載した上でAWS::NoValueを参照する

開発フロー/CI

テンプレートファイルはGitなどのバージョン管理システムを利用して管理することが望ましいでしょう。
筆者のチームではGitlabを利用して下記のフローで開発を進めています。

プレゼンテーション1.png

aws-cliのaws cloudformation validate-templateコマンドを実行することでフォーマットの検証を行うことで、実際にスタックを作成する前に、タイポ等の単純なミスを発見することができます。
より細かいチェックを行ってくれるcfn-lintというツールがあるみたいですが、筆者は未検証です。

ある程度の規模までは、このフローで問題なく運用できるはずです。
しかし規模が大きくなればなるほど次のような課題がでてきます。

  • テンプレートファイルが複数に分かれているため、環境横断的に各種リソースの設定値を見たり、横串で修正をしたりするのがつらい。
  • YAMLを書くのがそもそもつらい。
  • コーディング規約違反のチェックなどレビューもつらい。

そこで筆者のチームではCloudFormationの設定値をExcel, RDBで管理し、YAML自動生成するような仕組みを導入しています。
プレゼンテーション2.png

これにより、開発者はExcelだけをメンテナンスすればよくなりました。
同じAWSリソースは1シートに全て定義しているため、環境横断的にリソースの設定値を参照・修正することも容易です。

ただしこのような仕組みを作るのはそれなりに時間がかかりますし、汎用的に作ろうとするとある程度高度な設計も必要になってきます。ご紹介した方法は決して推奨するようなものではなくただの一例になりますが、何かしらのメンテナンスコストを下げるような仕組みがあると幸せになれると思います。

リリース

ここでのCloudFormationのリリースとはスタックを作成・更新・削除することを指します。
AWS CLIを利用してJOB等で実行させるなど色々な方法が考えられますが、スタックの操作については「管理コンソールからの実行」が一番良いと筆者は考えています。
理由としては「変更のプレビュー」により、AWSリソースの変更点が「視覚的」に確認できるためです。

console-changeset-details.png

スタックの更新を行う場合には必ず変更セットの作成から更新を行うようにしましょう。

CloudFormationを腐らせてはいけない

公式ドキュメントにも書かれていますが、AWS CloudFormationで作成したリソースをCloudFormation以外の方法で変更しては絶対にいけません。

スタックを起動した後、AWS CloudFormation コンソール、API、または AWS CLI を使用して、スタック内のリソースを更新します。スタックのリソースを AWS CloudFormation 以外の方法で変更しないでください。 変更するとスタックのテンプレートとスタックリソースの現在の状態の間で不一致が起こり、スタックの更新または削除でエラーが発生する場合があります。詳細については、「ウォークスルー: スタックの更新」を参照してください。

CloudFormationはあくまでCloudFormationの世界でAWSリソースを管理しており、管理コンソールから行った変更をいい感じに取り込んではくれません。最悪の場合、二度とCloudFormationが実行できなくなる可能性があります。

特にこの問題は、CloudFormationに詳しくない運用者に引継ぎを行う場合などに発生します。
本当の緊急事態を除き、原則AWSの管理コンソールはRead Onlyにしておくなど、権限制御を行いましょう。
(人を信じてはいけません。)

おわりに

まとまりなくつらつらと書いてしまいましたが、いかがでしたでしょうか。
CloudFormationを実際の現場でどう運用するのか考える際に、この記事が少しでも参考になれば幸いです。


  1. https://aws.amazon.com/jp/blogs/aws/aws-cloudformation-update-yaml-cross-stack-references-simplified-substitution/ 

  2. マルチリージョンでサービスを提供する場合はAWS契約単位、リージョン単位、環境単位、システム or サブシステム単位に分割したほうがいいでしょう。 

続きを読む

俺が結論づけたterraformベスト・プラクティスとworkspaceとmodule考え方(AWS Provider編)

俺です。
最強のテラフォーマーズなみなさんこんばんは。
久しぶりにterraformを弄っていて、かっこよくworkspaceとmodule使ってapplyキメたくなったので、どうつかったもんか、纏めたものです。

対応バージョン

  • 本体: terraform v0.11.1
  • provider: aws v1.5.0

terraform運用俺なりの結論

素terraformが一番ラク

それでも俺はWorkspaceやModuleを駆使するterraform新人類であり続けるぜ。むしろ既に触りすぎてて離れられない体だわはっはっはという俺な方は考え方読み飛ばしてどうぞ

考え方

  • 全部寺したいかたは寺どうぞ。
  • terraformは文学, CloudFormationは芸術, プルリクは人生, コードレビューは友情です。

環境構築

  • 設計思想やModule仕様をまとめたDocumentを常々更新するパワーがない
  • チームメンバーに伝達して一定品質保てるようなパワーがなければ素のterraformが楽です

運用

  • 全部寺でもいいけどLaunch/create/attach/destroyする、に留めるのがよいです
  • ルールの更新については俺達のCodenize.toolsで補えるものはこっちに寄せる方がRuby DSLによる表現力の高さや強力なDry-Runの恩恵を受けることができます。最高。
  • stateを持つコンポーネントであるRDSやElastiCache周りの構成変更はDestory判定から逃れられないものが多いのでCLIが安牌です

ex)ElastiCache Redisのreplication group memberの修正(Single-AZ->Multi-AZ化)

  • 1nodeのreplication group作成
  • number of nodeを1->2にするとDestroy -> Create判定になる

結局俺が選んだ最強のterraform設計is…?

  • Workspaceと異なり視覚効果に優れた方針を最強Opsの@shiruさんが公開してくれている。いきなりWorkspace/Moduleへ手を出す前に俺はコッチのほうをオススメしたい

それでもWorkspaceとModuleを使うんや!という方は次いってみましょー。

Workspaceについて

仮想的なディレクトリです。
terraform_workspace.png

resource内に ${terraform.env} を記述することでplan/apply時にcurrent workspace名が読み込まれ、冗長なコードを削減することができます。
上記図の例では、構築/運用対象の環境をworkspaceに収めることで、plan/apply実行時の構成変更対象を環境単位に実行することができます。

詳しくはssh絶対殺すマン@shogomuranushiさんのTerraform Best Practices in 2017を読むと良いです。

Workspaceの実装方法

ぼくはEC2原人なのでVPCを中心に実装方法を考えます。
今回の内容はregionについてはふれません。
マルチリージョンで構成される場合は他のWorkspaceの考え方がでてくるとも思います。

難易度 俺俺名称 VPCの状態 環境分離の方法
専用テナント型 環境ごとにVPCが分離されている場合 develop/stage/productionといった名称のVPCで環境が分離されている
共有テナント型 環境がVPCで分離されていない 1VPCとしてセキュリティグループレベルで環境が分離されている

専用テナント型

dedicated_workspace.png

全環境内に作るリソースを均一にできる場合はコレが最も楽です。
workspaceを切り替えてplan->applyでOKです。

terraform workspace new <環境名>
terraform workspace select <環境名>
module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
    subnet_id = "${data.terraform_remote_state.network.vpc.${terraform.env}.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

共有テナント型

shared_workspace.png

1VPCに複数の環境を作り、SGで分離する場合
これもworkspaceを切り替えてplan->applyでOKですが共有テナントを呼び出す時は${terraform.env}を書かないようにします。

terraform workspace new <環境名>
terraform workspace select <環境名>

★の箇所で${terraform.env}を指定せず、共有テナントが存在するworkspace名を直接指定する。

module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
★    subnet_id = "${data.terraform_remote_state.network.vpc.prod.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

Moduleについて

どこまでモジュール化するか。ちょっと悩みます。
なんでもモジュール化は良くないです。
モジュール化のレベルをキメておくのが良いです。
俺は2レベルでのモジュール化を推したいと思います。

public module

実体。俗に言うガワの定義
第三者に公開してもよい定義を表す。github.com などで公開してもよい。
リソース例:aws_vpc,aws_subnet,aws_instance,aws_eip等

private module

環境固有の情報が含れる属性を表す。
github.comのプライベートリポジトリやgithub enterprise上で管理する。
terraformにおいてガワと属性を分離して定義できないものもprivate moduleとして扱うのがよい。

リソース例:aws_security_group_rule, aws_elb等。ガワに対して中身をどうするかの定義。
SGの定義(企業のIPが入ってるもの)はPrivate moduleとして管理して使いまわせるとメンテが楽。
※aws_elbはELBとlistenerを分離して作成できないためprivate module化がよい。

やるな module

そもそもモジュール化してはならないモノ
実態と属性の関連付けを行うリソースはモジュールしないことが吉
リソース例: aws_elb_attachment,aws_eip_association等

Moduleの呼び出し例

  • 実体のvariable
variable "ad" {
    type = "map"
    default = {
        prod.ami = "ami-ec279c8a"  # Windows 2016
        prod.key_name = "XXXXXXXx"
        prod.public_key = "**********"
        prod.instance_type = "t2.medium"
        prod.iam_instance_profile = "ad"
        prod.source_dest_check = true
        prod.ebs_optimized = false
        prod.root_block_device = "gp2"
        prod.root_block_device_size = 64
        prod.ec2_count = 1
        prod.eip_count = 0
        prod.tag_name = "ad"
        prod.tag_role = "ad"
        prod.tag_amirotate = ""
    }
}
variable "ad_elb" {
    type = "map"
    default = {
        prod.name = "ad-elb"
        prod.instance_port = "3389"
        prod.availability_zones = ["ap-northeast-1a","ap-northeast-1c"]
        prod.access_logs_bucket = "XXXXXXXXXXX"
        prod.access_logs_bucket_prefix = ""
        prod.instance_protocol = "tcp"
        prod.lb_port = 3389
        prod.lb_protocol = "tcp"
        prod.lb_healthcheck_interval = 30
        prod.healthy_threshold = 2
        prod.unhealthy_threshold = 3
        prod.lb_target_timeout = 15
        prod.lb_healthcheck_interval = 5
        prod.cross_zone_load_balancing = true
        prod.idle_timeout = 60
        prod.connection_draining = 10
        prod.connection_draining_timeout = 15
    }
}
  • 実体の作成と関連付け
module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
    subnet_id = "${data.terraform_remote_state.network.vpc.prod.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

module "ad_elb" {
    source = "../../modules/provider/aws/loadbalancer/elb"
    elb = "${var.ad_elb}"
}

resource "aws_elb_attachment" "ad" {
  elb      = "${module.ad_elb.id}"
  instance = "${module.adelb.id}"
}

これで作り込みすぎないモジュール人生が送れると思います。

workspaceとmoduleの図解

2つを組み合わせるとこーんなかんじになります。
これの学習コストは高いか、低いかというと実装していて高いなって感じました。
workspace_and_module.png

terraformをこれから始めよう、とりあえず素terraformから卒業しようという方は、視認性の高いshiru式terraform設計で慣れてからworkspaceやmoduleへの取り組むステップを踏むと、workspaceとmoduleがよくわからないけどいいらしい。
から事故リスクを踏まえていい感じのterraform設計へつなげることができると思います。

それでは素晴らしいterraform生活を!

続きを読む

Hadoop 3 の GA を記念して S3Guard を試してみる

本記事は個人の見解であり、所属組織の立場、意見を代表するものではありません.

Distributed computing (Apache Hadoop, Spark, Kafka, …) Advent Calendar 2017 の 12/18 分です。

ついに Hadoop 3 が GA になりましたね!
本記事では Hadoop 3 の GA を記念して、新しい機能である S3Guard を試してみます。

S3Guard とは

Amazon S3 は広く知られている通り、整合性モデルとして以下の特徴をもちます。

  • 新しいオブジェクトの PUT に対する 書き込み後の読み取り整合性 (Read-after-write consistency)
  • 上書き PUT および DELETE に対する 結果整合性 (eventual consistency)

Hadoop から S3 を DFS として使う場合 S3A などを使う※わけですが、当然このような結果整合性の影響を受けます。
※ちなみに EMR では S3A ではなく EMRFS が推奨されています。

S3Guard とは S3A の拡張で、S3 上のオブジェクトのメタデータを別途保存・活用することで、Hadoop に整合性をもったビューを提供するための新機能です。
※他にもパフォーマンス改善を目的に含みますが、今回の説明では割愛します。

オフィシャルドキュメントはこちら。

Hadoop と S3 の関係性については imai_factory 先生が去年の Advent Calendar で詳しく解説してくれてますので、そちらをご参照ください。

Hadoop 3 環境の構築

では、早速環境を準備しましょう。
今回は以下の環境に Single Node Cluster を構築します。
– Amazon Linux 2 LTS Candidate AMI 2017.12.0.20171212.2 x86_64 HVM GP2
– c5.xlarge
– IAM ロール/インスタンスプロファイル付与 (DynamoDB/S3 のアクションをすべて許可)

まずは OpenJDK 1.8.0 をインストールして JAVA_HOME を export。

$ sudo yum install java-1.8.0-openjdk
$ export JAVA_HOME=/usr/lib/jvm/jre/

Hadoop 3 GA パッケージをダウンロードして配置。今回は riken さんのミラーを使います。

$ wget http://ftp.riken.jp/net/apache/hadoop/common/hadoop-3.0.0/hadoop-3.0.0.tar.gz
$ tar -zxvf hadoop-3.0.0.tar.gz
$ cd hadoop-3.0.0/

では早速サンプルを動かしてみましょう。正規表現にマッチしたものを表示します。

$ mkdir /tmp/input
$ cp etc/hadoop/*.xml /tmp/input/
$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.0.0.jar grep /tmp/input /tmp/output 'dfs[a-z.]+'
$ cat /tmp/output/*
1       dfsadmin

うまく動きました。

S3A

次に、S3A を使って S3 にアクセスしてみます。
S3A を使うには hadoop-aws-3.0.0.jar をクラスパスに含める必要があるので、hadoop-env.sh を以下のように編集します。

etc/hadoop/hadoop-env.sh
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:share/hadoop/tools/lib/*

これだけですね。では早速 S3A を使って S3 上の入力データを S3 に出力してみます。

$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.0.0.jar wordcount s3a://us-east-1.elasticmapreduce.samples/wordcount/data/ s3a://sample-bucket/hadoop3/output_wc/
$ bin/hdfs dfs -cat s3a://sample-bucket/hadoop3/output_wc/*

できました。WordCount の結果がどばーっと出ました。(ここでは長いので省略しています。)

S3Guard

ようやく本題。S3Guard を試してみます。
S3Guard を使うには core-site.xml に設定を追加する必要があります。
S3Guard ではメタデータ保存用のデータストアとして DynamoDB を使用します。S3 を使ってるんだから AWS アカウントをもってるだろうということで、DynamoDB を使うのは自然ですね。
ちなみに、EMR には EMRFS Consistent View という機能があり、そちらでも DynamoDB が使用されています。

etc/hadoop/core-site.xml
<property>
    <name>fs.s3a.metadatastore.impl</name>
    <value>org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore</value>
</property>

<property>
    <name>fs.s3a.s3guard.ddb.table</name>
    <value>s3guard-table</value>
</property>

<property>
  <name>fs.s3a.s3guard.ddb.region</name>
  <value>us-east-1</value>
</property>

<property>
    <name>fs.s3a.s3guard.ddb.table.create</name>
    <value>true</value>
</property>

それではサンプルジョブを実行します。

$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.0.0.jar wordcount s3a://us-east-1.elasticmapreduce.samples/wordcount/data/ s3a://sample-bucket/hadoop3/output_wc_s3guard/
$ bin/hdfs dfs -cat s3a://sample-bucket/hadoop3/output_wc_s3guard/*

できました。またもや WordCount の結果がどばーっと出ました。(長いので省略します。)

。。。と、S3Guard のあり/なしの影響がよくわからないですよね。
整合性が影響するようなワークロードじゃないと効果のほどはかなりわかりづらいです。
同時に複数のジョブがアドホックに走る状況で、結果整合性を許容できないようなワークロードで初めて生きてくるわけです。

ただ、DynamoDB に実際に保存されたメタデータの状態には興味が出てきますよね。
そこで、ジョブの実行が終わったこの時点のメタデータを見てみます。

まずはテーブルがちゃんとできているか Describe してみましょう。

$ aws dynamodb describe-table --table-name s3guard-table
{
    "Table": {
        "TableArn": "arn:aws:dynamodb:us-east-1:123456789101:table/s3guard-table",
        "AttributeDefinitions": [
            {
                "AttributeName": "child",
                "AttributeType": "S"
            },
            {
                "AttributeName": "parent",
                "AttributeType": "S"
            }
        ],
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0,
            "WriteCapacityUnits": 100,
            "ReadCapacityUnits": 500
        },
        "TableSizeBytes": 0,
        "TableName": "s3guard-table",
        "TableStatus": "ACTIVE",
        "TableId": "a0227da8-6f98-40b7-b973-67e83a206532",
        "KeySchema": [
            {
                "KeyType": "HASH",
                "AttributeName": "parent"
            },
            {
                "KeyType": "RANGE",
                "AttributeName": "child"
            }
        ],
        "ItemCount": 0,
        "CreationDateTime": 1513514910.2
    }
}

ちゃんとできてました。
それでは、このテーブルの中身をスキャンしてみましょう。
(出力がかなり長いので興味がない方は読み飛ばしてください。)

$ aws dynamodb scan --table-name s3guard-table
{
    "Count": 24,
    "Items": [
        {
            "is_dir": {
                "BOOL": true
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples"
            },
            "child": {
                "S": "wordcount"
            }
        },
        {
            "mod_time": {
                "N": "1513514946338"
            },
            "is_deleted": {
                "BOOL": true
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard/_temporary"
            },
            "child": {
                "S": "0"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "0"
            }
        },
        {
            "mod_time": {
                "N": "1513514944878"
            },
            "is_deleted": {
                "BOOL": true
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard/_temporary/0/_temporary/attempt_local1505268872_0001_r_000000_0"
            },
            "child": {
                "S": "part-r-00000"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "0"
            }
        },
        {
            "is_dir": {
                "BOOL": true
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount"
            },
            "child": {
                "S": "data"
            }
        },
        {
            "mod_time": {
                "N": "1513514948217"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard"
            },
            "child": {
                "S": "_SUCCESS"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1513514946328"
            },
            "is_deleted": {
                "BOOL": true
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard"
            },
            "child": {
                "S": "_temporary"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "0"
            }
        },
        {
            "mod_time": {
                "N": "1513514944034"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard"
            },
            "child": {
                "S": "part-r-00000"
            },
            "file_length": {
                "N": "733216"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1513514946347"
            },
            "is_deleted": {
                "BOOL": true
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard/_temporary/0"
            },
            "child": {
                "S": "_temporary"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "0"
            }
        },
        {
            "mod_time": {
                "N": "1513514946357"
            },
            "is_deleted": {
                "BOOL": true
            },
            "parent": {
                "S": "/sample-bucket/hadoop3/output_wc_s3guard/_temporary/0/_temporary"
            },
            "child": {
                "S": "attempt_local1505268872_0001_r_000000_0"
            },
            "file_length": {
                "N": "0"
            },
            "block_size": {
                "N": "0"
            }
        },
        {
            "mod_time": {
                "N": "1406157796000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0001"
            },
            "file_length": {
                "N": "2392524"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157796000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0002"
            },
            "file_length": {
                "N": "2396618"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157796000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0003"
            },
            "file_length": {
                "N": "1593915"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157796000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0004"
            },
            "file_length": {
                "N": "1720885"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157796000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0005"
            },
            "file_length": {
                "N": "2216895"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157797000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0006"
            },
            "file_length": {
                "N": "1906322"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0007"
            },
            "file_length": {
                "N": "1930660"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0008"
            },
            "file_length": {
                "N": "1913444"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0009"
            },
            "file_length": {
                "N": "2707527"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0010"
            },
            "file_length": {
                "N": "327050"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0011"
            },
            "file_length": {
                "N": "8"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "mod_time": {
                "N": "1406157798000"
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/us-east-1.elasticmapreduce.samples/wordcount/data"
            },
            "child": {
                "S": "0012"
            },
            "file_length": {
                "N": "8"
            },
            "block_size": {
                "N": "33554432"
            }
        },
        {
            "is_dir": {
                "BOOL": true
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/sample-bucket"
            },
            "child": {
                "S": "hadoop3"
            }
        },
        {
            "table_created": {
                "N": "1513514920327"
            },
            "table_version": {
                "N": "100"
            },
            "parent": {
                "S": "../VERSION"
            },
            "child": {
                "S": "../VERSION"
            }
        },
        {
            "is_dir": {
                "BOOL": true
            },
            "is_deleted": {
                "BOOL": false
            },
            "parent": {
                "S": "/sample-bucket/hadoop3"
            },
            "child": {
                "S": "output_wc_s3guard"
            }
        }
    ],
    "ScannedCount": 24,
    "ConsumedCapacity": null
}

入出力の両方に関係する S3 オブジェクトのメタデータが格納されていることが確認できます。

おわりに

今回は Hadoop 3 GA 記念ということで S3Guard を触ってみました。
Hadoop 3 には他にも面白い機能がたくさんありますので、どんどん試して使っていきたいですね。

続きを読む

TerraformでAWS AMIの最新を常に使うようにする

TerraformとAWSに同時入門する
の続きで、

最新のAMIが常に指定されるようなami設定にする

を行えるようにします。

確認環境

$ terraform version
Terraform v0.11.1
+ provider.aws v1.5.0

取得するAMIのスペック

今回は2017/12/10時点でのAmazon Linuxの下記スペックにおける最新AMI ID(ami-da9e2cbc)を取得します。
最新AMI IDはこちらで確認できます: Amazon Linux AMI ID

  • リージョン: アジアパシフィック東京
  • 仮想化タイプ: HVM
  • ルートデバイスタイプ: EBS-Backed
  • アーキテクチャ: x86_64
  • ボリュームタイプ: gp2

:warning: 注意点
AMI IDはリージョン毎に異なるようなので複数リージョンにまたがった環境の場合は注意が必要かもしれません

結論

簡易版

フィルタ条件はAMI Image名の条件のみ

aws_ami.tf
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*-x86_64-gp2"]
  }
}

詳細版

フィルタ条件として今回の指定スペックをすべて指定

参考: Terraformでもいつでも最新AMIからEC2を起動したい

aws_ami.tf
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
}

EC2側の設定

ec2.tf
resource "aws_instance" "web-server" {
  ami                    = "${data.aws_ami.amazon_linux.id}"
  instance_type          = "t2.micro"
}

確認

$ terraform plan

  + aws_instance.web-server
      id:                           <computed>
      ami:                          "ami-da9e2cbc"
      ...

AWS CLIでAmazon Linux AMI image名を取得してみる

準備

AWS Command Line Interface のインストール

簡易版のフィルタ条件の説明

AWS CLIで最新のAmazon LinuxのAMI IDを取得する
上記でのシェルを参考に今回取得したい条件に変更します

get-aws-ec2-image.sh.sh
...

describe_images(){
    echo $timestamp > $timestamp_file
    aws ec2 describe-images 
-       --owners self amazon 
+       --owners amazon 
        --filters 
+         Name=name,Values="amzn-ami-hvm-*" 
          Name=virtualization-type,Values=hvm 
          Name=root-device-type,Values=ebs 
          Name=architecture,Values=x86_64 
-         Name=block-device-mapping.volume-type,Values=standard
+         Name=block-device-mapping.volume-type,Values=gp2
}

...

実行結果

$ zsh ./get-aws-ec2-image.sh

amzn-ami-hvm-2014.03.2.x86_64-gp2: ami-df470ede
amzn-ami-hvm-2014.09.0.x86_64-gp2: ami-45072844
amzn-ami-hvm-2014.09.1.x86_64-gp2: ami-4585b044
amzn-ami-hvm-2014.09.2.x86_64-gp2: ami-1e86981f
amzn-ami-hvm-2015.03.0.x86_64-gp2: ami-cbf90ecb
amzn-ami-hvm-2015.03.1.x86_64-gp2: ami-1c1b9f1c
amzn-ami-hvm-2015.09.0.x86_64-gp2: ami-9a2fb89a
amzn-ami-hvm-2015.09.1.x86_64-gp2: ami-383c1956
amzn-ami-hvm-2015.09.2.x86_64-gp2: ami-59bdb937
amzn-ami-hvm-2016.03.0.x86_64-gp2: ami-f80e0596
amzn-ami-hvm-2016.03.1.x86_64-gp2: ami-29160d47
amzn-ami-hvm-2016.03.2.x86_64-gp2: ami-6154bb00
amzn-ami-hvm-2016.03.3.x86_64-gp2: ami-374db956
amzn-ami-hvm-2016.09.0.20160923-x86_64-gp2: ami-1a15c77b
amzn-ami-hvm-2016.09.0.20161028-x86_64-gp2: ami-0c11b26d
amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2: ami-9f0c67f8
amzn-ami-hvm-2016.09.1.20170119-x86_64-gp2: ami-56d4ad31
amzn-ami-hvm-2017.03.0.20170401-x86_64-gp2: ami-859bbfe2
amzn-ami-hvm-2017.03.0.20170417-x86_64-gp2: ami-923d12f5
amzn-ami-hvm-2017.03.1.20170617-x86_64-gp2: ami-bbf2f9dc
amzn-ami-hvm-2017.03.1.20170623-x86_64-gp2: ami-3bd3c45c
amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2: ami-4af5022c
amzn-ami-hvm-2017.03.rc-0.20170320-x86_64-gp2: ami-be154bd9
amzn-ami-hvm-2017.03.rc-1.20170327-x86_64-gp2: ami-10207a77
amzn-ami-hvm-2017.09.0.20170930-x86_64-gp2: ami-2a69be4c
amzn-ami-hvm-2017.09.1.20171103-x86_64-gp2: ami-2803ac4e
amzn-ami-hvm-2017.09.1.20171120-x86_64-gp2: ami-da9e2cbc
amzn-ami-hvm-2017.09.rc-0.20170913-x86_64-gp2: ami-d424e7b2

最新AMI ID(ami-da9e2cbc)が取得できているのがわかります。

amzn-ami-hvm-2017.09.1.20171120-x86_64-gp2: ami-da9e2cbc

取得結果からわかるようにImage名にはスペック名も含まれていることがわかります。

よって今回のスペックのAMI IDはImage名でのフィルタ条件のみで取ることができるため、簡易版ではImage名の値が"amzn-ami-hvm-*-x86_64-gp2"とするフィルタ条件1つで最新のAMI IDを取得していました。

また、aws_ami.tfではmost_recent = trueという設定により取得した中で最新のAMI IDが取得されます。

:warning: Image名の命名規則が変更された場合に指定スペックの最新が取得できなくなるので注意ください

フィルタ条件をnameのみにしてAWS CLIでも確認

AWS CLI側でも一応確認

get-aws-ec2-image.sh
describe_images(){
    echo $timestamp > $timestamp_file
    aws ec2 describe-images 
        --owners amazon 
        --filters 
          Name=name,Values="amzn-ami-hvm-*-x86_64-gp2" 
-          Name=virtualization-type,Values=hvm 
-          Name=root-device-type,Values=ebs 
-          Name=architecture,Values=x86_64 
-          Name=block-device-mapping.volume-type,Values=gp2
}
$ zsh ./get-aws-ec2-image.sh | grep ami-da9e2cbc

amzn-ami-hvm-2017.09.1.20171120-x86_64-gp2: ami-da9e2cbc

最新AMI ID(ami-da9e2cbc)が取得できているのがわかります。

その他の参考

続きを読む