まーぽんって誰がつけたの?

iOS→Scala→インフラなおじさん技術メモ

ECS(Amazon EC2 Container Service)がなんなのかやっと理解できてきた

いざ自分で一から作ってみると・・・

今まで人が作ったECS上でサービスの作成とか更新とか、タスクの更新とかやってきてなんとなく使えてたけど、 いざ自分で一から作ってみるとそれぞれの要素がどうなってるのか何も分かってないことに気づいた。 今まではECSでclusterとserviceを作っていって、clusterとかserviceに対して全体的なネットワークとかインスタンスの数とかの設定をしていくものだと思ってたけどそうじゃなかった。

ECSの役割

ECSがやってるのは主にスケジューラ(どのインスタンスにどのコンテナをいい感じに配置するか)がメインで、コンテナーインスタンスamazon-ecs-agentをおいてecsサービスのAPIを叩かせている。

じゃあ、全体的なネットワークの設定はどうするのかっていうと、ただのAuto Scalingという機能を使っていつも通りインスタンスを立ち上げてるだけ。なんならAuto Scalingを使わずに手動でAMIからインスタンスたちあげて、それをECSにコンテナーインスタンスとして登録したってよい。

GUIのウィザードでECSを作ろうとすると至れり尽せりでいい感じに色々なものができる。で、今やろうとしてるのはterraformで一から作ろうとしてるところだから、必要なものがなんなのか、どんな機能の組み合わせでできてるのかが分からないとterraformを書くことができない。進みは遅いし苦しいけど勉強にはなる。

ecs-agentの起動方法

会社の既存プロジェクトとかterraformのexampleを見てると、launch_configurationでuser_dataで結構がっつりシェルが書いてある。でも、AWS Consoleでウィザード経由で作ると、これだけ。

#!/bin/bash
echo ECS_CLUSTER=hoge >> /etc/ecs/ecs.config

一体この違いはなんなんだ・・ってはまったけどlinuxのdistributionが違ってた。見てたexampleとかはcoreOSとかubuntuとか。一方で、ウィザードだとamazon-linuxのecs-optimizedてやつ。だから色々書かずに済んでたんだ。 amazon-ecs-agentのREADMEにその辺のことが書いてあった。

まとめ

ちょっとすっきりした

インフラエンジニアになっていきたいと思った理由

今の思い

ここでいうインフラエンジニアはハードディスクがとかケーブルがとかガチなやつじゃなくて、各種スタックを組み合わせて構築するぐらいのやつ。 プログラミング歴=ほぼ社会人歴で、iOS開発がメインで一番長かった。そのあとScalaでサーバーサイド。で、今はインフラできるようになりたいと思ってる。

iOSエンジニアとしてサーバーサイドへの引け目

iOSは動く画面を作るのが楽しかった。ただ、すぐに太ってしまうUIViewControllerやたくさんのManagerクラスなどクソコードもいっぱい生み出してきてしまった。なので、Clean Architectureとかもっといい設計はないかとかを考えるのが楽しかった。 でも、なんとなくサーバーサイドとかWebのことが分からないことに引け目をずっと感じていたが、プロジェクトの状況で幸か不幸かサーバーサイドが足りないとなり、がっつりサーバーサイドの開発に携わることもできた。 はじめはWebの基本的なこととか効率的なSQL、DB設計とかが分かってなかったりScalaの関数型的な書き方が分からず苦しかったけどそこそこできるようになった。

解決できる領域の広さ

でも、サービスを運営してると障害起きたりしてもインフラ周りだと手も足も出せないのが悔しく、あと、何かXXXという問題を解決したいっていう時に、アプリケーション側で色々苦労して解決できたとしてもインフラ側でそれを何十倍の効果で解決しちゃったりする場面があって、それも悔しかった。なので、出来るようになりたい。

あとアプリもサーバーサイドも保守しやすいとか変更しやすいとかの設計を考えるのは楽しいし、実力もまだまだなんだけど、普通ぐらいのレベルにはなったなという気がしてる。

新しいことを覚える楽しさ

インフラは0だからそれを普通にする方がワクワクする。ほんの1ヶ月前は分からなかったことが分かるようになる感覚がたまらない。 ドラクエ3で言えば転職をしてレベルが1に戻るのを繰り返してる。 最初は簡単なことも分からず進捗も悪くてかなり苦しいんだけど、だんだんできるようになっていくのが好き。マゾなのかもしれない。

terraformというかHCLでif文使いたい

基本的にはサポートされてない。 けど、こちらの方のやり方を参考にして、こんな風にすればできるっちゃできる。

If you set count to 1 on a resource, you get one copy of that resource and if you set count to 0, that resource is not created at all.

blog.gruntwork.io

trueが1、falseが0になることを利用して、countを0または1にするっていうちょっとdirtyな感じ。

resource "aws_eip" "example" {
  count = "${var.create_eip}"
  instance = "${aws_instance.example.id}"
}

module "frontend" {
  source = "/modules/microservice"
  service_name = "frontend"
  ami = "ami-abcd1234"
  instance_type = "t2.medium"
  create_eip = true
}
module "backend" {
  source = "/modules/microservice"
  service_name = "backend"
  ami = "ami-efgh5678"
  instance_type = "m4.large"
  
  create_eip = false
}

なるほどねー。でも、公式にのってないようなやり方はあんまりやりたくないタイプ。

追記

terraform 0.8から公式にsupportされました🎉

resource "aws_instance" "web" {
  subnet = "${var.env == "production" ? var.prod_subnet : var.dev_subnet}"
}

CONDITION ? TRUEVAL : FALSEVAL

sts:AssumeRoleとは

AWS STSが分からなかった

AWS Security Token Serviceのこと。

aws stsググる

このドキュメントが出てきてざーっと読んでなんかCLIしたいときに使うのか?みたいなぐらいでしっくりこなかった。

docs.aws.amazon.com

EC2サービスを信頼するという概念

この記事が分かりやすかった。

さて、ここで封印した記憶を呼び起こします。「EC2サービス」を信頼することにより、「EC2がAssumeRoleを行えるように」なります。

dev.classmethod.jp

なるほどねー。

残業をさせてるのは誰

ふるさと納税のワンストップのやつ、自治体によって印字してくれてたり返信用封筒入れてくれてたりしてみんなこうなればいいのにと思ったんだけど、いいサービスやおもてなしを期待することにより、自分が巡り巡って残業や日本の生産性の低下を生み出してる側なのではと思った。

実際、過度なサービスに慣れすぎてしまってるなー。サービス受ける側としては日本最高だなーって思ってしまう

remote-execでEC2インスタンスの起動時の処理書いたけどuser-dataがあった

前にこれを書いたんだけど、こんなことする必要なかった。

mpon.hatenablog.com

こっちでよかったじゃないか。AWSのこと何も分かってない証拠ですね。

docs.aws.amazon.com

EC2をremote-execでprovisioningする方法

追記:そもそもuser-dataがあることを知らなかったのでこっちを使おう

www.mpon.me

elastic ipで接続されない

こんな風に素直に(自分的には)書くとElastic IPよりも先にEC2を先に作ろうとしちゃう。 で、private ipでssh接続しようとするから、そりゃあいつまでたっても接続できないし、 elastic ipも作られない。

resource "aws_eip" "dev" {
  instance = "${aws_instance.dev.id}"
  vpc = true
}

resource "aws_instance" "dev" {
  ami = "ami-b73b63a0"
  availability_zone = "us-east-1a"
  instance_type = "t2.micro"
  disable_api_termination = false
  key_name = "dev"
  
  vpc_security_group_ids = [
    "${aws_security_group.default.id}"
  ]
  
  subnet_id = "${aws_subnet.public.id}"

  provisioner "remote-exec" {
    connection {
      user = "ec2-user"
      private_key = "${file("${path.root}/dev.pem")}"
    }
    inline = [
      "sudo yum update -y"
    ]
  }
}

null_resourceを使う

これの最後の方のコメントが参考になった。 ec2_instance -> eip の順番で作成して、そのあとnull_resourceでtriggerを設定して、remote-execを実行する。 なるほどーうまい。

github.com

resource "aws_eip" "dev" {
  instance = "${aws_instance.dev.id}"
  depends_on = ["aws_instance.dev"]
  vpc = true
}

resource "aws_instance" "dev" {
  ami = "ami-b73b63a0"
  availability_zone = "us-east-1a"
  instance_type = "t2.micro"
  disable_api_termination = false
  key_name = "dev"
  
  vpc_security_group_ids = [
    "${aws_security_group.default.id}"
  ]
  
  subnet_id = "${aws_subnet.public.id}"
}

resource "null_resource" "preparation" {
  triggers {
    instance = "${aws_instance.dev.id}"
  }
  connection {
    host        ="${aws_eip.dev.public_ip}"
    user        = "ec2-user"
    timeout     = "30s"
    private_key = "${file("${path.root}/dev.pem")}"
    agent = true
  }

  provisioner "remote-exec" {
    inline = [
      "sudo yum update -y"
    ]
  }

}

まとめ

非同期に何かやりたい場合は、null_resourceがつかえそう

terraformのmoduleで定義したresouseにアクセスするにはoutputしないとダメ

moduleの誤解

何もわかってなかった。 module使うと変数とかをscope化できるからいいなーって思ってたけど、resourceもscope化されるってことだった。 まぁ逆に言えば、module内にresource名は被らないように気をつける必要がないとも言えるけど。 terraform how to get another module resource みたいな感じでググってもそんなにでてこないから多分当たり前の概念過ぎるんだろうな。

他のmoduleのresource名を取得するには

resource名から取ろうとしてもダメ

例えばこんなディレクトリ構成で、bastionの方でbase_networkで定義したvpcのidが欲しいみたいな場合。

├── base_network
│   ├── route_table.tf
│   ├── subnet.tf
│   └── vpc.tf
├── bastion
│   ├── ec2.tf
│   ├── eip.tf
│   └── security_group.tf
├── main.tf

vpc.tfで、resource "aws_vpc" "default"なんてやった場合に、bastionの中でaws_vpc.default.idってやっても、そんなresourceありませんよみたいな感じに言われてしまう。

外部から使いたい値をoutputする

なので、どうするかっていうと、各module内でoutputを定義する。

output "vpc_id" { value = "${aws_vpc.default.id}" }

で、これでbastionの方で、"${module.base_network.vpc_id}"ってやれば使えるんかなと思ったけど、使えない。 bastionの中で、moduleとして、base_networkをimportしないといけないんだと思う。 でも、そうすると依存関係がスパゲッティみたいになってしまうのが想像できる。 そこで、moduleを定義するときに、変数を受け取れるのでそこで受け取る。

module側はvariableで受け取る

main.tfが各moduleをimportするところなんだけど、そこで、変数の橋渡しをしてあげるイメージ。

provider "aws" {}

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

module "bastion" {
  source = "./bastion"
  vpc_id = "${module.base_network.vpc_id}"
}

で、bastion内で使うときは、"${var.vpc_id}"でいける。

最終的にはこんな感じになる。

├── base_network
│   ├── outputs.tf
│   ├── route_table.tf
│   ├── subnet.tf
│   ├── variables.tf
│   └── vpc.tf
├── bastion
│   ├── ec2.tf
│   ├── eip.tf
│   ├── outputs.tf
│   ├── security_group.tf
│   └── variables.tf
├── main.tf

これは、 terraform-community-modules · GitHubで使われてる基本的なやり方だった。やっとスタートラインにたどり着いた感じだw

terraformのresourceのNAMEの命名規則について

NAMEにTYPEを入れるかどうか

結構プロジェクトによってバラバラで他の言語でよくあるガイドラインみたいなのがない気がする。

個人的にはresourceのTYPEで特定できるから入れない方がいいかなという気になっている。

resource "aws_vpc" "dev_vpc_01" {}
resource "aws_vpc" "dev_vpc_02" {}

ではなくて、こっちの方がいいと思った。

resource "aws_vpc" "dev_01" {}
resource "aws_vpc" "dev_02" {}

とはいえ、IDEもないし、PRレビューとかで見る方としては冗長でもいいから書いてあった方が分かりやすいのではという気にもなってきた。

NAMEの規則

  • TYPEがスネークケースなので、NAMEもスネークケースがよさそう。
  • あとに環境が増えてもいいように001とか01とか通し番号つけておくとよさそう。

terraform入門時のメモ

どう入門するか

今まで人が作ったterraformの設定をいじって適用とかはしてきたけど自分で一から作ったことない。仕組みを理解するには、一から自分で作ってみるのが早そうだと思ったので自分で一から作ってみることにした。 ので、素直にこれをやることにした。

www.terraform.io

terraformで知ったこと

  • とにもかくにもproviderを書く。
  • terraform showコマンドがあること。これでtfstateが見れる。
  • planのときに出てくる-/+ は削除して再作成って意味
  • Atlasっていうterraformの設定ファイルバージョン管理 & 実行してくれるみたいなTerraformのremoteのサービスがある
  • depends_onで実行順序を変えられて、terraform graphコマンドで順序が見られる
  • 単にresourceを削除してterraform applyすればdestroyしてくれる
  • provisioningに失敗したらtaintedなresourceになって次はまた新しいのを再生成する
  • variableを{}にしておくとplan時に対話形式で入力することができる
  • variable "hoge" {} で変数宣言して使うときは、${var.hoge}、default値は宣言時のブレース内に書く
  • 変数は、環境変数か実行時の引数、実行時にファイル指定、terraform.tfvarsっていう決まった名前のファイルに書いておくことで渡せる
  • outputでapplyのときにコンソールに出力することができるし、markしておくおことができてterraform outputコマンドであとで簡単に取り出せる
  • providerがawsならAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGIONを読み込んでくれる

そもそもAWSについて知らなかったこと

  • ARN

Amazon リソースネーム (ARN) は、AWS リソースを一意に識別します。IAM ポリシー、Amazon Relational Database Service (Amazon RDS) タグ、API 呼び出しなど、明らかに全 AWS に渡るリソースを指定する必要がある場合、ARN が必要です。