c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud 等に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開発中です。

Terraformのmoduleを書く

AWS の設定を Terraform で行ってゐる。マネージメントコンソールからからやると簡單な設定であっても AWS CLI や Terraform からやらうとすると大變なものに成る場合も在る。コンソールでは一つに纏められてゐるものが個々別のオブジェクトとして其々操作しなければならないものや、裏で自動で作られるものも明示して作らねばならないものが在り、設定が膨れる。大變だ。一回だけなら膨れても何でも問題に成らないのだが、似たオブジェクトを沢山作らねばならぬと成れば、大變だ。否、似たものであれば纏められる筈である。Terraform にはモジュール機能が在る。

以前 API Gateway と Lambda を連携させて Terraform から設定た (API Gateway と Lambda の組み合はせでリリースするバージョンを制御する)。

此所で作ったものは、

有り體に此れ丈が API を作る度に必要に成る。死にたく成る。抽象化して呉れ。

書いてゐると判るが、以下の纏まりは重複する。Lambda は aws_lambda_function 毎に、

  • aws_lambda_function
  • aws_lambda_alias staging と prod
  • aws_lambda_permission staging と prod

が。API Gateway では aws_api_gateway_method 毎に、

が繰り返す。此れ等を其々モジュールに纏めてみる。

出來上がったものが以下に在るのでコードを読めば解ると云ふ話も在る。→ https://github.com/ne-sachirou/c4se-infra/tree/31bcdcaf8bc11c210163087c01b4087115d9eccd/terraform/modules

Terraform モジュールの構成

Terraform のモジュールは、Terraform のコードを分離する機能だ。モジュールを呼び出す時變數を與へる事が出來る。即ち、變數を切り替へる丈で重複させられるコードは、同じモジュールで扱へる。引數を與へ Terraform のコードを吐く函數の樣なものだ。

terraform-community-modulesに例が在る。

モジュールのファイルは、入力・本体・出力に分けて以下の樣にされる事が多い。

  • variables.tf モジュールで使ふ變數は全て此所で定義する
  • main.tf
  • outputs.tf モジュールの外から見える値は全て output 記述で書かねばならない。其れを全て此所に書く

例として以下の樣に使へる "a" と云ふモジュールを書く。

module "a_of_sample" {
  source = "./a"
  var1 = "sample"
}

resource b {
  attr = "${module.a_of_sample.out1}"
}

./a ディレクトリに三っつのファイルを作る。

# a/variables.tf
variable "var1" {}
# a/main.tf
resource some_resource {
  attr = "${var.var1}"
}
# a/outputs.tf
output "out1" {
  value = "${var.var1}"
}

ファイルの分け方は通常の Terraform と同じく任意なので、もっと細かく分けてもよい。variables.tf と outputs.tf は此う置いておくのが他人から見易いであらう。

モジュールを使った Terraform を実行する時には、plan を作る前に terraform get を行なふ。

terraform get
terraform plan --out=terraform.tfplan
terraform apply terraform.tfplan

GitHub 上等に在るモジュールを使ってゐれば .terraform/ に clone され、ローカルにモジュールを作ってあれば .terraform/ 下にシンボリックリンクが張られる。.terraform/ は.gitignore する。

モジュールに限らず Terraform では条件分岐は未だ出來ない。ループは少しは出來る。以下で實例を下[もと]に詳細を見てゆく。

API Gateway 向け Lambda のモジュール

  • aws_lambda_function
  • aws_lambda_alias staging と prod
  • aws_lambda_permission staging と prod

を纏めてみる。繰り返すコードをただ抽出するだけで出來る。

引數は、

variable "description" {}

variable "filename" {}

variable "function_name" {}

variable "handler" {}

variable "memory_size" {}

variable "prod_function_version" {
  default = "$LATEST"
}

variable "role" {}

variable "runtime" {}

variable "timeout" {}

でよいであらう。出力は、他から Lambda を呼び出すのに function_name が要るから、

output "function_name" {
  value = "${var.function_name}"
}

と成らう。するとメインは此う成る。

resource "aws_lambda_function" "function" {
  description = "${var.description}"
  filename = "${var.filename}"
  function_name = "${var.function_name}"
  handler = "${var.handler}"
  memory_size = "${var.memory_size}"
  role = "${var.role}"
  runtime = "${var.runtime}"
  source_code_hash = "${base64sha256(file(var.filename))}"
  timeout = "${var.timeout}"
}

resource "aws_lambda_alias" "staging" {
  depends_on = ["aws_lambda_function.function"]
  description = "staging"
  function_name = "${var.function_name}"
  function_version = "$LATEST"
  name = "staging"
}

resource "aws_lambda_alias" "prod" {
  depends_on = ["aws_lambda_function.function"]
  description = "production"
  function_name = "${var.function_name}"
  function_version = "${var.prod_function_version}"
  name = "prod"
}

resource "aws_lambda_permission" "staging_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.function"]
  function_name = "${var.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "staging"
  statement_id = "${var.function_name}_staging_apigateway"
}

resource "aws_lambda_permission" "prod_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.function"]
  function_name = "${var.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "prod"
  statement_id = "${var.function_name}_prod_apigateway"
}

此れ等を ./modules/aws_lambda_function_for_apigateway に保存しやう。呼び出しは此う成る。殆ど aws_lambda_function の内容だけで済む。

module "aws_lambda_function_for_apigateway_sample" {
  source = "./modules/aws_lambda_function_for_apigateway"
  description = "Sample."
  filename = "./sample.zip"
  function_name = "sample"
  handler = "handler.handler"
  memory_size = 128
  prod_function_version = "1"
  role = "${aws_iam_role.sample.arn}"
  runtime = "python2.7"
  timeout = 3
}

Lambda コードの ./sample.zip や Lambda 実行權限の aws_iam_role.sample.arn は今迄通り用意する必要が在る。本番デプロイして publish version した度に prod_function_version を上げてゆく。

Lambda 向け API Gateway のメソッドのモジュール

さて、先の Lambda を呼び出す API Gateway を作ってみる。

は今迄通り作っておく。aws_api_gateway_resource.sample_sample/sample を指すとする。

をモジュールにしやう。

モジュールの前に、aws_api_gateway_method_response と aws_api_gateway_integration_response をエラーのステータスコードの個數分作るのはループするのが有効である。ループは、洞の resource にも在る count 属性に依って行なへる。例へば、

resource a "a_with_count" {
  count = 2
  attr = "${count.index}"
}

は、

resource a "a_with_count_0" {
  attr = 0
}

resource a "a_with_count_1" {
  attr = 1
}

と、resource 名を除き等しい。又モジュールに配列は渡せないから、文字列分割函數 split 函數を使って、

variable "status_codes" {
  default = "400,500"
}

resource a "a_with_status_codes" {
  count = "${length(split(",", var.status_codes))}"
  status_code = "${element(split(",", var.status_codes), count.index)}"
}

とループ出來る。何とかして呉れ。

ループも出來る樣に成ったのでモジュール化する。マッピングも全て與へなければならないので引數はいささか多い。成るべく省略出來るやう default を定義しておく。

variable "authorization" {
  default = "NONE"
}

variable "aws_region" {}

variable "aws_account_id" {}

variable "error_status_codes" {}

variable "function_name" {}

variable "http_method" {}

variable "integration_request_parameters_in_json" {
  default = "{}"
}

variable "integration_request_templates" {
  default = <<EOF
{ "stage": "$stageVariables.stage" }
EOF
}

variable "integration_response_parameters_in_json" {
  default = "{}"
}

variable "integration_response_templates" {
  default = "$input.json('$')"
}

variable "method_request_parameters_in_json" {
  default = "{}"
}

variable "method_response_parameters_in_json" {
  default = "{}"
}

variable "resource_id" {}

variable "rest_api_id" {}

此れでもう大体見える。出力 (output) は不要だらう。メインは大体長い。長いからモジュール化したので仕方が無い。

resource "aws_api_gateway_method" "method" {
  authorization = "${var.authorization}"
  http_method = "${var.http_method}"
  request_parameters_in_json = "${var.method_request_parameters_in_json}"
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_id}"
}

resource "aws_api_gateway_integration" "integration" {
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  integration_http_method = "POST"
  request_parameters_in_json = "${var.integration_request_parameters_in_json}"
  request_templates = {
    "application/json" = "${var.integration_request_templates}"
  }
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_id}"
  type = "AWS"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${var.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "method_response_200" {
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_parameters_in_json = "${var.method_response_parameters_in_json}"
  rest_api_id = "${var.rest_api_id}"
  status_code = "200"
}

resource "aws_api_gateway_integration_response" "integration_response_200" {
  depends_on = [
    "aws_api_gateway_integration.integration",
    "aws_api_gateway_method_response.method_response_200"
  ]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_parameters_in_json = "${var.integration_response_parameters_in_json}"
  response_templates = {
    "application/json" = "${var.integration_response_templates}"
  }
  rest_api_id = "${var.rest_api_id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "method_response_error" {
  count = "${length(split(",", var.error_status_codes))}"
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_id}"
  status_code = "${element(split(",", var.error_status_codes), count.index)}"
}

resource "aws_api_gateway_integration_response" "integration_response_error" {
  count = "${length(split(",", var.error_status_codes))}"
  depends_on = ["aws_api_gateway_integration.integration"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_templates = {
    "application/json" = <<EOF
{ "error": "$input.path('$.errorMessage').substring(5)" }
EOF
  }
  rest_api_id = "${var.rest_api_id}"
  selection_pattern = "${element(split(",", var.error_status_codes), count.index)}: .+"
  status_code = "${element(split(",", var.error_status_codes), count.index)}"
}

aws_api_gateway_method_response.method_response_erroraws_api_gateway_integration_response.integration_response_error が count を使ってゐる。エラーメッセージは、「500: Some error.」等 \d{3}: が先頭に附くのを前提した。Terraform はモジュールに配列や連想配列を渡せないので此うしないとモジュールに出來ない。

此れ等を ./modules/aws_api_gateway_method_for_lambda_function に保存する。呼び出し例として GET /sample を作ってみる。

module "aws_api_gateway_method_research_random_id_get" {
  source = "./modules/aws_api_gateway_method_for_lambda_function"
  aws_region = "${var.aws_region}"
  aws_account_id = "${var.aws_account_id}"
  error_status_codes = "400,500"
  function_name = "${module.aws_lambda_function_for_apigateway_sample.function_name}"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.sample_sample.id}"
  rest_api_id = "${aws_api_gateway_rest_api.smaple.id}"
}

マッピングが default で定義したもので濟めば此の樣に短い。

やりましたね!

餘談

Lambda と API GatewayServerless Framworkに載せ替へたので上記モジュールは不要と化しましたがわたしは元氣です。

Switch API Gateway from Terraform to Serverless Framwork.