LoginSignup
7
5

More than 3 years have passed since last update.

Infrastructure as CodeのTips【Terraform/CDK/Cloudformation】

Posted at

TL; DR

普段色々なツールを使ってインフラコード化を細々としています。
自分の中で少し溜まってきたので整理と備忘録も兼ねて書いていきます。

Terraform

みんな大好きTerraformです。筆者も大好きです。
なんとなくで書けちゃうのがいいんですよね。とりあえずplan投げてエラー見て直してみたいな。
Terraformのおかげでエラー出ても焦らなくなりました。(initで出るとちょっと自分にムッとはしますがw)
多機能な分、追えていなかったり避けている機能がちょこちょこあったので使ってみるか〜くらいでやったのですがよかったものをピックアップしてます。

for_each

名前から繰り返しの処理をするやつなんだろうな、と思いつつcountで避けてきたfor_eachくん。
慣れてしまえば多用しそうになるやつです。
aws_subnetとかで使ってます。便利です。
resourceブロックでしか利用できないので注意です。

count style
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"
}
for_each style
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が苦手な筆者はこっちのがシンプルやんけ!と書けたときに小躍りしました。
リソースのインデックスもわかりやすいのでプラン結果やデプロイ中も見守りやすいのも理由です。

plan result
  # 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.tf
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なんかはちょうどいいのかな?と思います。

multiple written
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"
  }
}
dynamic written
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を使ってユーザーやロールの出し分けをしています。

旧バージョンのユーザー作成の部分だけ抜き出しています。
ユーザーはベタがきでコードにそのまま書いています。

old style
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として利用できるように呼び出しています。

using cdk.json
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`
      });
    })
  }
}
cdk.json
{
  "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"
}

こんな感じのファイル構成をしています。
divisionprjはタグ付けなどに利用していますので、特に環境ごとの配下にしていません。
これらをデプロイするときはcdk deploy -c stage=devと言った感じで出し分けています。
本当は環境ではなくアカウントナンバーにしてstackの中でアカウントナンバーを取得してからcontextを注入したかったのですが、その前にcdk.jsonが読み込まれてしまうため実現できませんでした。
今は一旦これでいいかな、という感じで落ち着いています。

Cloudformation

AWSの初期からある(たしか…)インフラコード化の走りみたいな存在ですね。
正直あんまり使ってこなかったのですが、AWS CDKの利用に伴い色々と理解が必要だったので使っています。
yaml形式自体なので可読性は高いと思うのですが、リソース名や独自の関数など覚えることいっぱいです。
まあ時間をみつけてゆるゆるとやっている感じです。

ユーザーデーターが使える

完全にシェルこねこねと思っていたのですが、Cloudformationに普通に書けました。

userdata template
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なりで使いまわせるようにしましょう。
ベタがき、不揃いは後々つらくなってきます…。

terraform

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
  }
}
cdk.json
{
  "context": {
    "Name": "hoge",
    "Service": "fuga",
    "Env": "hogefuga"
  }
}
Cloudformation
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のコードか見づらいじゃないの…布教できない…。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5