Amazon ECSを用いたDocker本番運用の実現

はじめに

現在お手伝いしているアカウンティング・サース・ジャパンにて、ECSを使ったDockerの本番運用を始めたので、その一連の流れについてまとめました。

税理士向け会計システムを扱うアカウンティング・サース・ジャパンでは最近Scalaでの新規プロジェクトが立ち上がってきており、既存のプロジェクトはJavaであったり、Erlangであったりと様々な言語が用いられていますが、インフラ人員が少ないということもあり、なるべくシンプルなインフラ構成を実現する必要がありました。

そういった中、各アプリケーションをDocker化することでインフラとしては共通基盤としてのDockerクラスタのみの管理になり、運用コストが下がるのではないかという仮説からDocker化を進めることになりました。クラスタを実現するに辺りKubenatesなどの選択肢もありましたが、今回はECSを選択し、下記のようにAWSのマネージドサービスを最大限に活用しています。

  • オーケストレーションツール: Amazon EC2 Container Service (ECS)
  • サービスディスカバリ: Application Load Balancer (ALB)
  • Dockerレジストリ: Amazon ECR
  • ログ、メトリクス収集: CloudWatch, CloudWatch Logs
  • 監視: CloudWatch Alarms
  • Infrastructure as Code: CloudFormation
  • CIツール: Jenkins

各技術の選定理由

今回Docker化を行うに辺り、下記を優先的に技術選定を行いました。

  • 運用が楽であること
  • 構成がシンプルで、技術の学習コストが低いこと

まずは、オーケストレーションツールの選定です。候補に上がったのは、Docker Swarm、Kubernetes、ECSです。

DockerのSwarm modeは本番での運用例が技術選定時点であまり見当たらなかったので候補から落としました。次にKubernetesとECSですが、海外の事例などではどちらも多く使われているようです。

今回は多機能さよりも運用に手間がかからない方が良いと考え、マネージドサービスであるECSが第一候補にあがりました。ここは詳細に調査したというよりも、ある種勢いで決めています。その上でやりたいことが実現できるかどうか一つ一つ技術検証を行った上で導入判断を行いました。

同じようにマネージドサービスを優先的に使ったほうが良いという考えで、ログなどでもCloudWatchを使っています。

AWSインフラをコードで記述するものとしてはTerraformが良く取り上げられている気がしますが、個人的にはいくつかの理由でCloudFormationを推しているのでこちらを使っています。

CIツールですが、社内の標準であるJenkinsをそのまま使うことにしました。

全体構成

下記のような構成になっています。

スクリーンショット 2017-05-21 12.46.39.png

ざっくりと説明すると、developmentブランチにプッシュするとGithub HookでJenkinsがDockerイメージをビルドして、ECRにPushします。ユーザはJenkinsでDeployジョブを実行(あるいはBuildの後続ジョブとして自動実行)し、CloudFormationにyamlファイルを適用することでTask, Service, ALB, Route53設定, CloudWatch設定を一通り実行します。またECSのClusterはあらかじめCloudFormationテンプレートを作成して作っておきます。

Task/Serviceの更新についてはCloudFormationを経由しない方がシンプルかとは思いまいしたが、Service毎に管理するRoute53やCloudWatchと合わせて一つのテンプレートにしてしまうのが良いと判断しました。

ここまでやるなら専用のデプロイ管理ツールを作った方がとも思ったのですが、業務委託という立場で自分しかメンテができないものを残すものは躊躇されたため、あくまでAWSとJenkinsの標準的な機能を組み合わせて実現しています。

CloudFormationテンプレートの解説

上記の流れが全てなので理解は難しくないと思いますが、一連の処理で重要なポイントとなるのはCloudFormationテンプレートなのでこれについてだけ触れておきます。長いテンプレートなのでざっくりとだけ雰囲気を掴んでもらえればと思います。

ECSクラスタのテンプレート

cluster作成用のCloudFormationテンプレートは下記のようになっています。

gist:cluster.yaml

一見複雑に見えますが、Amazon EC2 Container Service テンプレートスニペットを参考に作ると簡単に作成できると思います。

(あまりそのまま書くと会社に怒られそうなため)省略していますが、実際にはここにECSクラスタの監視を行うCloudWatch Alarmなどを設定することで、監視設定までこのテンプレートだけで完了します。

ECSクラスタはインフラチーム側であらかじめ用意しておき、リソースが足りなくなったときなどには適宜インスタンス数を変更したりクラスタ自体を別途作ったりしていきます。オートスケーリングを導入すればそれすら必要なくなります(今回はDocker運用が初めてだったので知見がたまるまで手動での対応にしています)。

インフラ側としての責務はここまでで、下記のテンプレートで定義される個別のサービスについてはアプリ開発者側の責務として明確に責任境界を分けました。(もちろん実際にはサポートはかなりの部分でしています。)

これにより全員が今までよりインフラに近い領域まで意識するように個人の意識が変わっていくことを期待しています。

個別サービス用テンプレート

開発環境、ステージング環境、プロダクション環境などそれぞれで同一のテンプレートを使うようにし、パラメータを使用します。そのパラメータをJenkinsのジョブ内で注入することで実現します。VPCなどの環境で決まる値はJenkinsジョブで実行するスクリプト内で定義し、アプリケーションごとの値は environment.yaml というファイルを用意してスクリプトから読み込みます。

environment.yamlは例えば下記のようになっています。アプリケーション開発者は、特殊なことをしない限りは service.yaml をインフラチームが用意したservice.yamlをコピーして、environment.yamlだけ編集すれば良い形になっています。DSLですら無いのでアプリ側のメンバーも心理的な抵抗が少ないようで良かったです。

environment.yaml
images:
- xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-image
parameters:
  default:
    TaskMemory: 512
    TaskMaxMemory: 990
    ImageRepositoryUrl: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-image
    ServiceDesiredCount: 1
  dev:
    ClusterName: dev-default
    JavaOpts: "-Xmx256MB"
  stg:
    ClusterName: stg-default
    JavaOpts: "-Xmx256MB"
  prod:
    ClusterName: default
    JavaOpts: "-Xmx1500MB -Xms1500MB"
    TaskMemory: 1990
    TaskMaxMemory: 1990
    ServiceDesiredCount: 2

そして service.yaml は下記のようなファイルです。

gist:service.yaml

これもAmazon EC2 Container Service テンプレートスニペットから作ればすぐにできるのではないかと思います。(もちろん全てのパラメータは一つ一つ値を検討します。)

こちらもCloudWatch周りや重要でないところは削除しています。色々と手で削ってるのでコピペだと動かない可能性大ですが雰囲気だけ掴んで貰えればと思います。

このファイルは全アプリケーションで同一ファイルを使うのではなく、アプリケーションごとにコピー/編集して利用します。全体の変更を行うときには全プロジェクトのファイルを更新しなければいけませんが、共通基盤がアプリケーション側を制約しないように、プロジェクト毎のyamlファイル管理としています。ファイルの配置場所は各Gitリポジトリに配置するのが理想ですが、現状ではDocker運用になれてくるまで全てのyamlファイルを管理するリポジトリを作成してインフラチーム側が主に編集する形を取っています。

デプロイ

あとは、このservice.yamlとenvironment.yamlを組み合わせてデプロイするRubyスクリプトでもJenkinsのPipelineのコードでも適当に書いてJenkinsのJobを登録すれば完了です。(environment.yamlファイルを読み込んで aws cloudformation create-stack でservice.yamlと共にパラメータとして渡すだけなので簡単です!)

新規アプリ開発時も社内標準のservice.yamlとenvironment.yamlをファイルを持ってきて、environment.yamlを修正した上で、Jenkinsにジョブを登録すればすぐにDockerクラスタへのデプロイ準備が整います。しかも、上記のテンプレート例では割愛していますが、テンプレートには監視項目/通知設定まで書かれているので、インフラ側で設定を行う必要もなく監視が開始されます。CloudFormation最高ですね。

おわりに

実際の運用ではミッションクリティカルなアプリケーションならではの品質管理のために、JenkinsのPipeline機能を利用して開発→検証→リリースまでのデプロイメントパイプラインを実現しています。

アプリケーションのSECRETなどコミットしない情報をどう管理するかも検討する必要がありますが、これは管理の仕方はチームによって異なると思ったため割愛しています。

また、ログ解析としてはS3に出されたALBのログをRedash+Amazon Athenaでエラー率やアクセス数を分析できるようにし、CPU使用率やメモリ使用率などのパフォーマンス状況をCloudWatchの内容をGrafanaで可視化しています。これによりログ収集の基盤などを作らずに必要な可視化を実現することができました。ベンチャーでは分析基盤の運用も大きなコストになってしまうため、こういった工夫も必要です。(もちろん重要なKPIについては別途分析する仕組みが整っています。)

今回の構成が最高とは思いませんが、ある程度満足行くところまではできたかなと思います。もっとよくできるよ!とか一緒にやりたいな!とかもっと詳細聞きたいな!いう方はぜひ @miyasakura_ までご一報ください。

続きを読む

Terraform Best Practices in 2017

Terraform Best Practices in 2017

以下のブログをベースにver0.9の新機能のstate environmentsや、backend、remote stateを活用してベストプラクティスを考えた。
細かい話は以下のブログを参照いただき、ver0.9に対応した内容だけ記載します。
Terraformにおけるディレクトリ構造のベストプラクティス | Developers.IO

サンプルコード

サンプルコードを置きましたので、イメージが付かない場合は以下を見てみて下さい。
(適当に作ったので間違えてたらプルリクください)
https://github.com/shogomuranushi/oreno-terraform

ディレクトリ構造

├── environments
│   ├── not_immutable
│   │   ├── provider.tf
│   │   ├── backend.tf
│   │   ├── variable.tf
│   │   ├── main.tf
│   │   └── output.tf
│   └── immutable
│       ├── provider.tf
│       ├── backend.tf
│       ├── variable.tf
│       ├── main.tf
│       └── output.tf
└── modules
    ├── compute
    │   ├── ec2.tf
    │   ├── elb.tf
    │   ├── output.tf
    │   ├── userdata.sh
    │   └── variable.tf
    ├── db
    │   ├── main.tf
    │   ├── output.tf
    │   └── variable.tf
    └── vpc
        ├── main.tf
        ├── output.tf
        └── variable.tf

ディレクトリ構造のポイント

1. environments配下の分け方

  • tfstateファイルで管理する範囲が大きいと問題があった際の影響範囲が大きくなるため実行単位を小さくする
  • 今回の場合は、not_immutableとimmutableで分けて、それぞれの配下でterraformを実行する
  • terraformの実行単位を分けるとterraform間での値の受け渡しが通常とは異なり、 remote state 機能を利用する必要がある
    • 以下のようにdataを定義することで、remote側のoutputを参照できるようになる

    • 注意点
      • 制約としてremote先のmoduleの先のoutputは読み取れないのでmodule直下(root)でoutputを定義する必要がある
      • tfstateの管理方法をs3にした状態でstate environmentsを使った時のs3のprefixは env:/<environment>/ なるため以下のように記述する
remote_state参照方法(backend.tf内に記述)
data "terraform_remote_state" "not_immutable" {
  backend = "s3"
  config {
    bucket = "< backetname >"
    key    = "env:/${terraform.env}/not_immutable/terraform.tfstate"
    region = "< region >"
  }
}
moduleへの渡し方
module "compute" {
    source         = "../../modules/compute"

    vpc            = "${data.terraform_remote_state.not_immutable.vpc}"
}

2. dev/stg/prodなどの環境の分け方

  • ver0.9以前はdev,stg,prodなどはディレクトリを分けることで、tfstateを競合させないようにしていたが、ver0.9で追加された state environments を利用して環境を分ける
  • terraform env new dev を打つことでdevの環境が作られる

    • デフォルトでは直下に terraform.state.d というディレクトリができ、その配下に環境毎にtfstateが管理される
  • terraform env list を打つことで現在のenviromentを参照可能

state_environmentsの実行方法
$ terraform env new dev

$ terraform env list
  default
* dev
  stg
  • その前にtfstateはs3に置いたほうが良いと思うので、以下の記述も入れて terraform init を実行することでtfstateをs3で管理出来る状態になる。その後に terraform apply を実行することでtfstateが生成される

    • なお、s3をbackendにすると /env:/< environment > が補完され < backetname >/env:/< environment >/immutable/terraform.tfstate のように管理される
backend.tf
terraform {
  backend "s3" {
    bucket = "< backet name >"
    key    = "immutable/terraform.tfstate"
    region = "us-west-2"
  }
}
initの実行方法
$ terraform init

state environmentsの活用方法

今まではdevやstg、prodを別のディレクトリで管理していたため、それぞれのディレクトリにvariableを置くような形だったが、state environmentsの登場により1つのディレクトリで複数の環境を扱えるようになった。
そこで如何に効率的に複数の環境を扱えるか考えた結果、以下になった。

  • map関数をガンガン使う

    • environments配下のvariable.tfには以下のようにmapで定義する
  • map関数のkeyの部分をドット区切りでenv情報を入れる
    • 環境毎に値を定義出来る

      • env毎の切り替え方法は、値取得時に "vpc-${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
        }
        のように ${terraform.env} にdevやstgが入りvalueとして参照可能になる
    • envの値が無ければdefaultを参照するように定義する方法は以下
      • defaultの指定方法は "vpc-${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
        }
        のように ${lookup(key, value, default) で指定可能

それらを踏まえたコードは以下

variable側
variable "common" {
    default = {
        default.region     = "us-west-2"
        default.project    = "oreno-project"

        dev.region         = "us-west-2"
        stg.region         = "us-west-2"
        prd.region         = "ap-northeast-1"
  }
}

# VPC
variable "vpc" {
    type = "map"
    default = {
        default.cidr       = "10.0.0.0/16"
        default.public-a   = "10.0.0.0/24"
        default.public-c   = "10.0.1.0/24"
        default.private-a  = "10.0.2.0/24"
        default.private-c  = "10.0.3.0/24"
    }
}
module呼び出し時
module "vpc" {
    source       = "../../modules/vpc"
    common       = "${var.common}"
    vpc          = "${var.vpc}"
}
module内からvariableの値を取得する時
resource "aws_vpc" "vpc" {
    cidr_block                  = "${lookup(var.vpc, "${terraform.env}.cidr", var.vpc["default.cidr"])}"
    enable_dns_support          = "true"
    enable_dns_hostnames        = "true"
    tags {
        Name                    = "vpc-${lookup(var.common, "${terraform.env}.project", var.common["default.project"])}"
    }
}
参考:こんな感じでproviderでもlookup可能
provider "aws" {
    region = "${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
}

まとめ

  1. 影響範囲を小さくするため、terraformの実行単位は小さくしましょう
  2. terraform間の受け渡しは remote state を使いましょう
  3. state environments を使って環境を分けましょう
  4. map関数を使ってmodule等に渡す時などのコードを簡素化しましょう
  5. map関数 & state environments & default定義を使ってvariableを効率化させましょう

以上

続きを読む

Terraformのoutputでmapを利用する方法

やること

Terraformのoutputでmapを利用する

なにが嬉しいか

module間で値を受け渡す時のoutputの定義が短くなる

Befor: 冗長的なoutput

output側の定義
output "vpc_id" {
  value = "${aws_vpc.vpc.id}"
}

output "public-a" {
  value = "${aws_subnet.public-a.id}"
}

output "public-c" {
  value = "${aws_subnet.public-c.id}"
}

output "private-a" {
  value = "${aws_subnet.private-a.id}"
}

output "private-c" {
  value = "${aws_subnet.private-c.id}"
}
moduleから呼び出す時
module "compute" {
  source = "../../xxx"

  vpc_id     = "${module.xxx.vpc_id}"
  public-a   = "${module.xxx.public-a}"
  public-c   = "${module.xxx.public-c}"
  private-a  = "${module.xxx.private-a}"
  private-c  = "${module.xxx.private-c}"
}

After: mapを使ったoutput

outputの際にvalueの値に "${map("key", "value")}" を入れることでmapとして利用可能になる。

output側の定義
output "vpc" {
  value = "${
    map(
      "vpc_id",           "${aws_vpc.vpc.id}",
      "subnet-public-a",  "${aws_subnet.public-a.id}",
      "subnet-public-c",  "${aws_subnet.public-c.id}",
      "subnet-private-a", "${aws_subnet.private-a.id}",
      "subnet-private-c", "${aws_subnet.private-c.id}"
    )
  }"
}
moduleから呼び出す時
module "compute" {
  source = "../../xxx"
  vpc = "${module.xxx.vpc}"
}

スッキリ!!

参照方法

ちなみに以下のようにlookupでkeyを指定することでvalueを参照出来る。

.tf
subnet_ids = ["${lookup(var.vpc, "subnet-private-a")}"]}

続きを読む

S3バケットへのアクセスを特定IAMユーザにだけ許可して他は弾く

社内共有用にメモ。S3バケットへのアクセスを特定IAMユーザにだけ許可したい

課題

単純に考えると、バケットポリシーを次のようにすれば、s3-audit ユーザだけがアクセスできる xxxx-audit バケットが作れそうである。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
              "AWS": "arn:aws:iam::xxxx:user/s3-audit"
            },
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xxxx-audit",
                "arn:aws:s3:::xxxx-audit/*"
            ]
        }
    ]
}

しかしコレだけだと、PowerUser のような S3 へのアクセスポリシーがついている IAM ユーザでもアクセスすることができてしまう。バケットへのアクセス権限は、IAM ポリシーとバケットポリシーの複合になるので(正確にはもっと色々関わる)、IAMポリシーで許可が出ているとダメ。

BucketPolicy IAM 結果
拒否 拒否 失敗
拒否 許可 失敗
許可 拒否 失敗
許可 許可 成功
未設定 拒否 失敗
未設定 許可 成功

ref. http://dev.classmethod.jp/cloud/aws/s3-acl-wakewakame/
※ IAMポリシーで許可が出ていると、BucketPolicyが未設定でも、通ってしまう(成功)点に注目

解答

結果的には次のようになった。Principal: "*" とした Deny の中で、"aws:username": "s3-audit" のように s3-audit ユーザだけ除外しているのがポイント。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
              "AWS": "arn:aws:iam::xxxx:user/s3-audit"
            },
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xxxx-audit",
                "arn:aws:s3:::xxxx-audit/*"
            ]
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xxxx-audit/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:username": "s3-audit"
                }
            }
        }
    ]
}

補足

前述の解答だと、Deny の方の Resource に arn:aws:s3:::xxxx-audit を指定していないので、ListBucket はできてしまう (PowerUser な人が Web Console から bucket にあるオブジェクトの一覧を見れてしまう。ダウンロードはできない)。

arn:aws:s3:::xxx-audit を指定しなかったのは、これをやると terraform を apply しているユーザもアクセスできなくなって、ポリシーを変更できなくなってしまうため。

以下のように StringNotEquals に terraform 用の IAM アカウントも指定すれば問題ないといえばないのだけど、terraform 用の IAM アカウントからもバケットにアクセスできるようになってしまって、表題の要件を満たせなくなる。悩ましい。

 {
     "Version": "2012-10-17",
     "Statement": [
         {
             "Effect": "Allow",
             "Principal": {
               "AWS": "arn:aws:iam::xxxx:user/s3-audit"
             },
             "Action": "s3:*",
             "Resource": [
                 "arn:aws:s3:::xxxx-audit",
                 "arn:aws:s3:::xxxx-audit/*"
             ]
         },
         {
             "Effect": "Deny",
             "Principal": "*",
             "Action": "s3:*",
             "Resource": [
+                "arn:aws:s3:::xxxx-audit",
                 "arn:aws:s3:::xxxx-audit/*"
             ],
             "Condition": {
                 "StringNotEquals": {
-                    "aws:username": "s3-audit"
+                    "aws:username": ["s3-audit", "terraform-user"]
                 }
             }
         }
     ]
 }

続きを読む

Terraformでstate lockを触ってみた。

はじめに

初めてTerraformを触る折に
「v0.9以上だとstate lockっていう機能があるから使ったほうが良いよ」
というアドバイスをもらいました。
(そもそも、stateってなんですか?から始まるわけですが・・・)

初めてTerraformを触った時に、公式ドキュメントだけでは???な状態だったので
痕跡として残しておきます。

結果として、まだ使いこなせてません。というか僕の使い方は果たしてあっているのだろうか
なので情報としては不足しているかもしれませんが、とりあえず備忘録的に残します。
またわかったらUpdateします。

そもそもState Lockとは

完全に理解しないで書いてますが
Terraformは「自分が知っているリソース(EC2やS3)しか関与しないよ」というポリシーで
どのリソースを自分が扱ったか、というのをtfstateというJSONファイルで管理しているようです。

このtfstateファイルは、一人でTerraformを動かす場合は問題無いのですが
複数人でTerraformをいじる必要が出てきた時に問題で、それは「backend」モジュールを使うことで回避してきたようですが
同じタイミングでterraformを実施した場合、その部分までは制御しきれてなかったようです。

で、v0.9以上?から、「Plan/Applyをしているタイミングではロックする」機能が実装されたようで。
せっかくなので導入してみました。

公式サイト:https://www.terraform.io/docs/state/locking.html

準備

手動で準備が必要なものは
・terraformを実行するユーザのCredential情報
→ めんどくさかったので test-terraformとかいうユーザを別途作成しました。
・S3 Bucketの作成
→ terraform-s3-state とかいう名前で作りましょう
・DynamoDBの作成
→ 名前はなんでも良いです。terraform-state-lock とかで作りましょう。
  プライマリキーを「LockID」としてください。それ以外では動きません。
作り終えたら、読み込み・書き込み容量ユニットは最低の1にしておいたほうが良いと思います。

※ S3とDynamoDBをterraformで管理はしないほうが良さげです。
どのみち、初回実行時に「そんなリソース無いんだけど!」ってTerraformが怒ります。

動くまで

今回はState Lockの話がメインなので作るリソースはなんでも良いです。
リージョンが関係無いRoute53を題材にします。

route53.tf
# 適当に1ゾーン作ります。

resource "aws_route53_zone" "test_zone" {
    name    =   "test.lockstate.com"
}
settings.tf
# ネーミングよくないですが、providerとbackendの設定します

provider "aws" {
    access_key = "ACCESS_KEY"
    private_key = "PRIVATE_KEY"
}

terraform {
    backend "s3" {
        bucket     = "terraform-s3-state"
        key        = "terraform.tfstate"
        region     = "s3とdynamoがいるregion"
        lock_table = "terraform-state-lock"
    }
}

これで、「該当のディレクトリに移動して」$ terraform plan してください。(怒られます。)

Backend reinitialization required. Please run "terraform init".
Reason: Initial configuration of the requested backend "s3"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

とりあえず terraform init しろよと言われるので言われるがままに。

$ terraform init

ただし、以下の通り、認証情報がねーんだけど!って怒られる。なんやねん。

Initializing the backend...

Error configuring the backend "s3": No valid credential sources found for AWS Provider.
  Please see https://terraform.io/docs/providers/aws/index.html for more information on
  providing credentials for the AWS Provider

Please update the configuration in your Terraform files to fix this error
then run this command again.

ここらへんちゃんとよくわかってないですが、この問題の対処として2つありました。
A. settings.tfのbackendにaccess_key/secret_keyの2つを渡してCredential情報を平文で書く。
-> 変数でいいじゃん!と思ったんですが、変数で渡すと、Terraformがよしなにやる「前に」
  Backendが立ち上がるために違う方法で渡してくれ、と怒られました。
以下のようなメッセージ

% terraform init 
Initializing the backend...
Error loading backend config: 1 error(s) occurred:

* terraform.backend: configuration cannot contain interpolations

The backend configuration is loaded by Terraform extremely early, before
the core of Terraform can be initialized. This is necessary because the backend
dictates the behavior of that core. The core is what handles interpolation
processing. Because of this, interpolations cannot be used in backend
configuration.

If you'd like to parameterize backend configuration, we recommend using
partial configuration with the "-backend-config" flag to "terraform init".

B. 環境変数にaccess_key/secret_keyの2つを食わせる
  → 今はこちらを採用中。

init or plan実行時に、状況に応じて「ローカルのStateをS3にコピーするか / S3からStateファイルをローカルにコピーするか」聞かれるかもしれません。
初回(もしくはテスト)であればYes、すでに何回か実行しているような状況ではnoでいいんじゃないでしょうか。

これで、terraform plan (or apply)を実行すると
DynamoDBのLockDBに対して誰が使用しているか、のロック情報が書き込まれ
終わったタイミングでリリースされます。

ターミナルやシェルを複数立ち上げ、同じぐらいのタイミングで
terraform plan( or apply )を実行すると、ロックが成功できた奴以外はエラーで落とされます。
ただし、ロックがリリースされる前の実行中などにCtrl-Cなどで強制終了させるとロックが残り続けるので注意。
$ terraform force-unlock ID情報
で強制ロック解除できます。

今僕が解決できてない問題点

公式ドキュメントを見る限り、「terraform remote コマンドは terraform initコマンドに置き換えたよ!」と言っています。
また、backendを使用する、backendを書き換えた、などのタイミングに起因して terraform init を求められます。

が、terraform initは、「実行されたディレクトリに対して .terraform/terraform.tfstate」を吐き出します。
そしてそれが無いと怒ります。
まだ試せてはいませんが、以下のようなtfの配置をしていたりした場合
root配下でinitを実行しても、tfファイルがありませんと怒られる状況で
tfファイルがあるディレクトリまで移動しないとinitができない状態です。

$ terraform init ./route53/tf
とするとroot直下にtfファイルがコピーされる状況です。
なので、リソースごとに区切る、とか環境ごとに区切る、とかはうまくできていません。
別の見方をすれば、環境ごとに一つのディレクトリでリソース類は全部その中で管理しろよ!
というHashiCorpからのお達しなのか・・・?
これは、新しく実装されたEnvironmentを試せ、ということなんでしょうか。。。

と思ったらBackendConfigなるものがあるらしいのでそれを試してみよう。

root
├── settings
│   └── tf
│   └── settings.tf
├── route53
│   └── tf
│   └── route53.tf
└── ec2
└── tf
└── ec2.tf

Terraformとの付き合い方がよくわからない

続きを読む

terraform importの使い方メモ

terraform importの使い方メモ

背景

terraformではv0.7.0以降で実装されたimportコマンドを使用して既存インフラをterraform管理下に置くことができます。といってもtfstateへのインポートのみです。しかもv0.9.2時点では一括インポートができないため、リソースIDを指定してインポートする必要があります。さらにその後terraform planで差分がなくなるまでtfファイルを作り込む面倒な作業が待っています。そのような事情もあって現時点ではdtan4/terraformingを使用する人が多いと思われますが、やはり対応リソースの広さに惹かれて本家の機能を使ってみました。

本題

事前にモジュール構造を決める

とりあえず手で環境を作ってHashicorpのbest practiceに従ったモジュール構造でコード化しようと試みました。作った環境は上記のコードとだいたい同じで、パブリックのサブネットとプライベートなサブネットがある単純なVPCです。(詳細は割愛)

インポート

何はともあれ公式のリファレンスに従ってインポートしてみます。vpc-idはマネジメントコンソールから拾ってきます。リソースの多い場合はSDKなどでIDを引っこ抜いてくるスクリプトを作ったほうが良いと思います。

$ terraform import aws_vpc.vpc vpc-********

次にvpcのtfファイルを作成し、planを実行しました。しかし既存リソースは定義なしと見なされ再作成の対象となってしまいます。

$terraform plan

- aws_vpc.vpc

+ module.network.vpc.aws_vpc.vpc

原因は一見してわかるようにモジュール構造が異なるためです。そこでモジュール指定でインポートしてみると、今度はリソース名を解決できないと怒られます。

$ terraform import module.network.vpc.aws_vpc.vpc vpc-********
Error importing: failed to parse resource address 'module.network.vpc.aws_vpc.vpc': Unexpected value for InstanceType field: "vpc"

モジュールの指定方法

インポートの際はplanの表示と違ってvpcモジュール側もmodule.vpcと指定する必要があったのでした。apply等と同じ仕様ですね。

$ terraform import module.network.module.vpc.aws_vpc.vpc vpc-********

この状態でterraform planを実行するとめでたく差分なしとなりvpcは破壊されなくて済むのでした。terraformの管理下におく場合はとりあえずインポートしてからtfファイルを作り始めるというよりモジュール構造を決めてからインポートした方が効率的と思います。

インポートできないリソース

ルートテーブルなどに紐づく関連付けやセキュリティグループのルールは個別のインポートができない仕様です。ルートテーブルに複数の関連付けなどがある場合自動的にリソース名の末尾に数字が付いた名前で別々のリソース定義としてインポートされてきます。

ここで気になるのが、例えばルートテーブルに対する関連付けを動的に生成するような管理をしたい場合です。例えば下記のようなコードでprivateルートテーブルに対する関連付けを一括して管理したいとします。

resource "aws_route_table_association" "private" {
  count          = "${length(var.private_subnet_ids)}"
  subnet_id      = "${element(var.private_subnet_ids, count.index)}"
  route_table_id = "${element(aws_route_table.private.*.id, count.index)}"
}

しかしimportは上位のルートテーブル単位でしかできません。

terraform import module.network.module.route_table.aws_route_table.private rtb-*******

このコマンドの結果はaws_route_table_association.privateaws_route_table_association.private-1などどと別々の定義としてtfstateに書き出されます。こうなると事後でstateに合わせるためにはtfファイルに関連付けの数だけリソース定義を書かないといけなくなりそうです。stateと差分がなければコードなんて何でもいいだろという思想も一理ありますが、やはりここは保守性や拡張性なども考えたいところです。

逆にtfstateをコードに合わせる

推奨はしませんがtfstateを手で編集するか、terraformをカスタマイズしてpull requestを出してしまうかですが、余程腕に自信のあるエンジニアでない限り前者の方が手っ取り早いでしょう。aws_route_table_association.private-1aws_route_table_association.private.1
などどコードに合わせて書き換えてしまいましょう。(自己責任)

tfstateの書き換えは下記の記事を参考にさせて頂きました
Terraforming未対応の既存リソースも自力でtfstateを書いてTerraform管理下に入れる

結論

まだまだ未完成の機能なので単にインポートするだけで綺麗にコード化して管理できるとはいかないようです。v1.0が待たれます。

続きを読む

Terraform v0.8からTerraform v0.9へのアップグレード S3の状態管理ファイルを移行方法

Terraformをアップグレードした際にS3に保存していた状態管理ファイルも移行する必要が
あったのでその作業メモです。

Terraformのアップグレード

https://www.terraform.io/downloads.html から環境に適したファイルを
ダウンロードして、解凍して作成されるterraformファイルをパスの通ったところに
配置します。
例えば以下のようにパスを通しておきます。

bash_profile
export PATH=$PATH:${HOME}/.terraform_0.9.3_darwin_amd64

状態管理ファイルの移行

Terraformのアップグレード自体は数分で終了しますが、planコマンドを実行すると以下の
メッセージが表示されます。

$ terraform plan
Deprecation warning: This environment is configured to use legacy remote state.
Remote state changed significantly in Terraform 0.9. Please update your remote
state configuration to use the new 'backend' settings. For now, Terraform
will continue to use your existing settings. Legacy remote state support
will be removed in Terraform 0.11.

You can find a guide for upgrading here:

https://www.terraform.io/docs/backends/legacy-0-8.html

There are warnings related to your configuration. If no errors occurred,
Terraform will continue despite these warnings. It is a good idea to resolve
these warnings in the near future.

どうやら、Terraformがv0.9になったタイミングで状態管理ファイルの管理方法が変更になったようです。
メッセージを見る限りではv0.11までは変更しなくても使えそうな感じですが、
https://www.terraform.io/docs/backends/legacy-0-8.html ではv0.10になったタイミングで
移行作業が必要になりそうなので、上記URLの手順に沿って新しい状態管理ファイルに移行してみます。

既存の状態管理ファイルの取得

最新の状態管理ファイルをremoteから取得します。

$terraform remote pull

ローカルの状態管理ファイルのバックアップ

.terraform/terraform.tfstateファイルをterraformのプロジェクトの外に置きます。

$cp .terraform/terraform.tfstate /tmp/

backendの設定の追加

backendの設定ファイルを作成します。
今までもS3に状態管理ファイルを置いていたので以下のURLを参考にしながら
backend.tfファイルを作成しました。
https://www.terraform.io/docs/backends/types/s3.html

backend.tf
terraform {
  backend "s3" {
    bucket = "terraform-state"
    key    = "tf"
    region = "ap-northeast-1"
  }
}

initコマンドの実行

backendの初期化コマンドを実行します。

$ terraform init -backend=true -force-copy  -lock=false
Initializing the backend...
New backend configuration detected with legacy remote state!

Terraform has detected that you're attempting to configure a new backend.
At the same time, legacy remote state configuration was found. Terraform will
first configure the new backend, and then ask if you'd like to migrate
your remote state to the new backend.




Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your environment. If you forget, other
commands will detect it and remind you to do so if necessary.

上記のようなメッセージが表示され、移行が無事に行われたようです。

terraform planで動作確認

最後にterraform planで動作確認をしてみます。
Deprecation warningが消えて通常の状態に戻りました。

$ 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.
...

続きを読む

[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

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

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

セキュリティグループの作成

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        }
    ]

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

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

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

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む