TL; DR
普段色々なツールを使ってインフラコード化を細々としています。
自分の中で少し溜まってきたので整理と備忘録も兼ねて書いていきます。
Terraform
みんな大好きTerraformです。筆者も大好きです。
なんとなくで書けちゃうのがいいんですよね。とりあえずplan
投げてエラー見て直してみたいな。
Terraformのおかげでエラー出ても焦らなくなりました。(init
で出るとちょっと自分にムッとはしますがw)
多機能な分、追えていなかったり避けている機能がちょこちょこあったので使ってみるか〜くらいでやったのですがよかったものをピックアップしてます。
for_each
名前から繰り返しの処理をするやつなんだろうな、と思いつつcount
で避けてきたfor_each
くん。
慣れてしまえば多用しそうになるやつです。
aws_subnet
とかで使ってます。便利です。
resource
ブロックでしか利用できないので注意です。
resource "aws_subnet" "public_subnet" {
count = length(split(",",lookup(var.availability_zones,default))) // 2とかでもOK
vpc_id = aws_vpc.service_vpc.id
cidr_block = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, count.index)
availability_zone = element(split(",", lookup(var.availability_zones, default)), count.index)
map_public_ip_on_launch = true
}
variable "availability_zones" {
default = "ap-northeast-1a,ap-northeast-1c"
}
resource "aws_subnet" "public_subnet" {
for_each = var.availability_zones
vpc_id = aws_vpc.service_vpc.id
cidr_block = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, each.value)
availability_zone = each.key
map_public_ip_on_launch = true
}
variable "availability_zones" {
default = {
"ap-northeast-1a" = 0
"ap-northeast-1c" = 1
}
}
こんな感じで違った書き方ができます。
なんとなくTerraformのFunctions
が苦手な筆者はこっちのがシンプルやんけ!と書けたときに小躍りしました。
リソースのインデックスもわかりやすいのでプラン結果やデプロイ中も見守りやすいのも理由です。
# module.service_vpc.aws_subnet.public_subnet["ap-northeast-1a"] will be created
+ resource "aws_subnet" "public_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "ap-northeast-1a"
+ availability_zone_id = (known after apply)
+ cidr_block = "192.168.0.0/20"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "ENV" = "sandbox"
+ "Name" = "terraform-sand-public-ap-northeast-1a"
+ "Service" = "terraform"
}
+ vpc_id = (known after apply)
}
# module.service_vpc.aws_subnet.public_subnet["ap-northeast-1c"] will be created
+ resource "aws_subnet" "public_subnet" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "ap-northeast-1c"
+ availability_zone_id = (known after apply)
+ cidr_block = "192.168.16.0/20"
+ id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ map_public_ip_on_launch = true
+ owner_id = (known after apply)
+ tags = {
+ "ENV" = "sandbox"
+ "Name" = "terraform-sand-public-ap-northeast-1c"
+ "Service" = "terraform"
}
+ vpc_id = (known after apply)
}
これらのリソースをアウトプットするときは少し工夫が必要でfor
文で回してあげる必要があります。
output "public_subnets" {
value = [for subnet in aws_subnet.public_subnet : subnet.id] // 欲しいattributeは変数から取る
}
// NG例
output "public_subnets" {
value = aws_subnet.public_subnet.*.id
}
output "public_subnets" {
value = aws_subnet.public_subnet[*].id
}
dynamic block
複数設定可能な項目などは繰り返し書くのが面倒だったり、環境によって必要・不要が分かれていたりすることもあると思います。
そう言った部分を良きに計らってくれる優れものです。個人的には使いすぎはよくないとは思うのでaws_db_parameter_group
なんかはちょうどいいのかな?と思います。
resource "aws_db_parameter_group" "mysql_56" {
name = "${var.service_name}-${var.env}-${var.engine}-56"
family = "mysql5.6"
parameter {
name = "character_set_server"
value = "utf8"
apply_method = "pending-reboot"
}
parameter {
name = "character_set_client"
value = "utf8"
apply_method = "immediate"
}
parameter {
name = "performance_schema"
value = "1"
apply_method = "pending-reboot"
}
}
resource "aws_db_parameter_group" "mysql_56" {
name = "${var.service_name}-${var.env}-${var.engine}-56"
family = "mysql5.6"
dynamic "parameter" {
for_each = var.db_params
content {
name = each.value.name
value = each.value.value
apply_method = each.value.apply_method
}
}
variable db_params {
default = {
"server_character" = {
name = "character_set_server"
value = "utf8"
apply_method = "pending-reboot"
},
"client_character" = {
name = "character_set_client"
value = "utf8"
apply_method = "immediate"
},
"insight" = {
name = "performance_schema"
value = "1"
apply_method = "pending-reboot"
}
}
}
いや、変数冗長じゃね?と思うかと思いますが、module
などを利用して環境ごとに出し分けているとこういうcount
とかが使えないものが出てきてつらみに陥ることがあるんです…。それがresource
同じでmodule
側だけ書き換えるみたいな運用ならまあ許容できるかなと思ってます。(そもそもそんな環境構成にするなよ、みたいなのはまた別のお話しです。)
AWS CDK
最近は細々としたリソースをつくるときに利用したりしています。
ある程度まとまったリソース単位でStackとして利用できるので中々好きです!AWS公式ですし、Typescriptの型の恩恵受けまくりで書きやすいしエラーの発見も簡単です。
新しいリソースへの対応も早いので、ローンチされたばかりのサービスでもガンガン使えちゃいます!(AWS-SDK-JSとかからインポートして使う、が大体ですが。)
cdk.json
cdk init
すると自動で作成されるものですが、うまく使うと便利です!
以前IAMを管理している記事を書いたものをちょこちょことアップグレードしていて、今はcdk.json
を使ってユーザーやロールの出し分けをしています。
旧バージョンのユーザー作成の部分だけ抜き出しています。
ユーザーはベタがきでコードにそのまま書いています。
import { Construct, Stack, StackProps } from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');
const admins = 'testAdminGroup';
const adminUsers = [
'demo01',
];
const developers = 'testDevGroup';
const devUsers = [
'demo02',
'demo03'
];
export class IamUserStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Admin group
const adminGroup = new Group(this, admins, { groupName: admins });
adminGroup.addManagedPolicy(adminPolicy);
adminGroup.attachInlinePolicy(commonPolicy);
// Developer group
const devGroup = new Group(this, developers, { groupName: developers });
devGroup.addManagedPolicy(powerUserPolicy);
devGroup.attachInlinePolicy(devPolicy);
devGroup.attachInlinePolicy(commonPolicy);
// Create users
adminUsers.forEach(adminUser => {
new User(this, adminUser, {
userName: adminUser,
groups: [adminGroup],
});
});
devUsers.forEach(devUser => {
new User(this, devUser, {
userName: devUser,
groups: [devGroup]
});
});
}
}
cdk.json
を利用しているパターンはこちら。
5-14行目でcdk.json
をcontextとして利用できるように呼び出しています。
import { Aws, Construct, Stack, StackProps, App, CfnOutput, Fn, Arn } from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');
const app = new App();
const stage: any = app.node.tryGetContext("stage")
// Admin Users Configure
const admins: string = 'adminGroup';
const adminUsers = app.node.tryGetContext(stage).adminUsers // contextとしてユーザーをcdk.jsonから呼び出している。
// Develop Users Configure
const developers: string = 'devGroup';
const devUsers = app.node.tryGetContext(stage).devUsers
export class IamUserStack extends Stack {
/** @returns the ARN of the IAM User */
public readonly userArn: string;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Admin group
const adminGroup = new Group(this, admins, { groupName: admins });
adminGroup.addManagedPolicy(adminPolicy);
adminGroup.attachInlinePolicy(commonPolicy);
// Developer group
const devGroup = new Group(this, developers, { groupName: developers });
devGroup.addManagedPolicy(powerUserPolicy);
devGroup.attachInlinePolicy(devPolicy);
devGroup.attachInlinePolicy(commonPolicy);
// Create Admin users
adminUsers.forEach((adminUser: string) => {
const adminUserName = new User(this, adminUser, { userName: adminUser, groups: [adminGroup] });
new CfnOutput(this, `${adminUserName}`, {
value: adminUserName.userArn,
description: `${stage} environment admin user arn`,
exportName: `${stage}AdminUsersArn`
});
})
// Create Develop users
devUsers.forEach((devUser: string) => {
const devUserName = new User(this, devUser, { userName: devUser, groups: [devGroup] });
new CfnOutput(this, `${devUserName}`, {
value: devUserName.userArn,
description: `${stage} environment develop user arn`,
exportName: `${stage}DevUsersArn`
});
})
}
}
{
"context": {
"division": "hogehoge",
"prj": "fugafuga",
"dev": {
"adminUsers": [
"demo01"
],
"devUsers": [
"demo02",
"demo03"
]
},
"stg": {
"adminUsers": [
"circleci",
"github"
],
"devUsers": [
"jenkins"
]
},
"prd": {
"adminUsers": [
"circleci",
"github"
],
"devUsers": [
"jenkins"
]
}
},
"app": "npx ts-node bin/iam_user.ts"
}
こんな感じのファイル構成をしています。
division
やprj
はタグ付けなどに利用していますので、特に環境ごとの配下にしていません。
これらをデプロイするときはcdk deploy -c stage=dev
と言った感じで出し分けています。
本当は環境ではなくアカウントナンバーにしてstackの中でアカウントナンバーを取得してからcontextを注入したかったのですが、その前にcdk.json
が読み込まれてしまうため実現できませんでした。
今は一旦これでいいかな、という感じで落ち着いています。
Cloudformation
AWSの初期からある(たしか…)インフラコード化の走りみたいな存在ですね。
正直あんまり使ってこなかったのですが、AWS CDK
の利用に伴い色々と理解が必要だったので使っています。
yaml
形式自体なので可読性は高いと思うのですが、リソース名や独自の関数など覚えることいっぱいです。
まあ時間をみつけてゆるゆるとやっている感じです。
ユーザーデーターが使える
完全にシェルこねこねと思っていたのですが、Cloudformationに普通に書けました。
Resources:
BastionLaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: bastion-launch-template
LaunchTemplateData:
UserData:
Fn::Base64: !Sub
- |
#!/bin/bash
# timezone
timedatectl set-timezone "Asia/Tokyo"
# time sync
yum -y remove ntp
yum -y install chrony
echo '#Add TimeSync' >> /etc/chrony.conf
echo 'server 169.254.169.123 prefer iburst' >> /etc/chrony.conf
systemctl start chrony
systemctl enable chrony
共通
リソース名の命名規則は最初から揃えて、変数なりParameter
なりで使いまわせるようにしましょう。
ベタがき、不揃いは後々つらくなってきます…。
variable "Name" { default = "hoge" }
variable "Service" { default = "fuga" }
variable "Env" { default = "hogefuga" }
resource "hogehoge" "fugafuga" {
/////////////省略/////////////
tags = {
Name = "${var.service_name}-${var.env}-fugahoge"
Service = var.service_name
Env = var.env
}
}
{
"context": {
"Name": "hoge",
"Service": "fuga",
"Env": "hogefuga"
}
}
Parameters:
Name:
Description: resource name.
Type: String
Default: hoge
Service:
Description: service name.
Type: String
Default: fuga
Env:
Description: environment name.
Type: String
Default: hogefuga
##############################
Tags:
-
Key: "Name"
Value: !Ref Name
-
Key: "Service"
Value: !Ref Service
-
Key: "Env"
Value: !Ref Env
おまけ
どうでもいいけどなんでQiitaのコードブロックからHCL
を消してしまったのでしょう…。
Terraformのコードか見づらいじゃないの…布教できない…。