業務でTerraformを数ヶ月使用しました。内容を忘れた時に見返す用としてメモします。
業務ではモジュールを使用したフォルダ構成を主に扱ったため、それらを中心に記載しています。
(Workspacesの記載はありません。)
#環境構築
##AWSCLIインストール
以下を参考にAWSCLIをインストールします。
##awsプロファイル設定
~/.aws
フォルダに以下のようなcredencal
,config
ファイルを作成します。
[dev]
aws_access_key_id = アクセスキーを設定
aws_secret_access_key = シークレットアクセスキーを設定
通常は以下のように記述します。
[dev]
output = json
region = ap-northeast-1
スイッチロールを使用する場合は以下のように記述します。
[dev]
output = json
region = ap-northeast-1
[profile dev_switchroll]
role_arn = arn:aws:iam::アカウントID:role/スイッチロール先で使用するロール名
source_profile = dev <- スイッチ前のプロファイルを指定
region = ap-northeast-1
output = json
設定したプロファイルは名前を設定しているのでawsコマンドでは--profile
オプション指定が必要です。
$ aws sts get-caller-identity --profile dev
$ aws sts get-caller-identity --profile dev_switchroll
##terraformインストール
Terraformのインストールは、tfenvを使用するとバージョンを切り替えられるようになるので便利です。
# git リポジトリをクローンしてパスを通すだけ。
$ git clone https://github.com/tfutils/tfenv.git ~/.tfenv
$ echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
# 再ログインまたは.bash_profileを読み込み設定を反映すると完了。
$ source ~/.bash_profile
以下を実施してterraformコマンドを使用できるようにする。
# リモートにあるTerraformのバージョン確認
$ tfenv list-remote
# Terraformのインストール(latestまたはバージョンを指定)
$ tfenv install latest
$ tfenv install バージョン
# インストール済みのTerraformのバージョン確認
$ tfenv list
# Terraformのバージョン切り替え(latestまたはバージョン指定)
$ tfenv use latest
$ tfenv use バージョン
# Terraformのバージョン確認
$ terraform version
##stateファイル保存先作成
stateファイルの保存先はS3にするとチームで共有できるのでおすすめです。
後述するterraform_settings.tf
内で保存先を指定します。指定しない場合、ローカルにstateファイルが作成されます。
以下はCloudFormationの簡単な例です。
AWSTemplateFormatVersion: '2010-09-09'
Description: 'create S3 Bucket'
Parameters:
Env:
Type: String
AllowedValues:
- none
- dev
- stg
- prd
Default: none
Projectname:
Type: String
Default: 'sample'
Resources:
StoreSourcesBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join ['-', [!Ref Projectname, !Ref Env, 'tfstate', !Ref "AWS::AccountId" ]]
# バージョニングしない場合はEnabledではなくSuspendedを指定する。
VersioningConfiguration: {"Status": "Enabled"}
実行例
$ ls
template.yml
$ aws cloudformation create-stack \
--profile dev \
--stack-name create-s3-bucket-for-test \
--template-body file://./template.yml \
--parameters ParameterKey=Env,ParameterValue=dev
#Terraformよく使うコマンド
Terraformでよく使うコマンド一覧。
terraform init # 初期化
terraform plan # 計画実行(定義内容の確認)
terraform apply # 適用
terraform destroy # リソースの廃棄(削除)
terraform fmt -recursive # フォーマット
terraform state list # リソースを一覧表示
terraform state mv # リソースブロックなどの名前変更
terraform show # リソース情報表示
terraform import # 既存リソースからTerraform定義を作成する
この中でもinit
,plan
,apply
,destroy
の使用頻度が高いです。
コマンドは上記以外にもあります。
詳細はドキュメントを参照してください。
(ちょっとわかりずらいですが、左メニューから各コマンドの詳細に飛べます。)
#作業の流れ
定義作成後のリソース作成は、以下のように初期化、確認、適用、不要になったら削除という流れで作業していました。
# 初期化(プロファイルを指定する場合は -backend-config="profile=dev"を付ける)
$ terraform init
# 計画実行(定義内容の確認)
$ terraform plan
# 適用
$ terraform apply
# リソースの廃棄(削除)
$ terraform destroy
特定のリソース、モジュール、またはリソースのみ適用したり削除したい場合は、-target
オプションを使用します。
詳しくは以下を参照してください。
$ terraform plan -target="module.s3_bucket"
$ terraform plan -target={module.sample.aws_ecs_cluster.ecs_cluster,module.sample.aws_ecs_service.ecs_service}
定義作成についてはAWSマネジメントコンソールでリソースを一旦手動で作成しterraform import
で定義化したり(後述します)、サービス名 terraform
で検索してリソースのドキュメントを参照しつつ作成したりしていました。
以下のドキュメントの左メニューから対象のサービスを探すのもありです。
#フォルダ構成
携わった業務ではモジュール形式でTerraformを使用することが多く、その際のフォルダ構成が以下のような形でした。
各tfファイルについては後述します。
terraform-repository gitリポジトリとかで管理。
┗ sample # システムや任意の単位でフォルダを作成(この単位でterraformを実行、stateファイルが作成される)
┗ env
┗ dev # applyの実行場所。環境単位でフォルダを作成。
┗ files
┗ iam_role_policy_for_hoge.json # IAM用のjsonファイルなど定義で使用するファイルを保存
┗ terraform_settings.tf # Terraformのバージョンやプロバイダーのバージョンを指定
┗ provider.tf # プロバイダーの情報を設定
┗ data.tf # 既存のリソースの情報取得を定義
┗ variable.tf # 変数を定義
┗ main.tf # モジュール呼び出しを定義
┗ output.tf # 作成したリソースをモジュール外などで使用するための出力を定義
┗ module
┗ sample1 # モジュール単位でフォルダを作成
┗ iam.tf # 各リソース作成用の定義を記入したtfファイルを配置
┗ ec2.tf
┗ output.tf
┗ sample2
┗ sample_sub1 # さらに階層を増やしても良い(モジュール呼び出し時にsourceで指定する)
┗ iam.tf
┗ sample_sub2
Terraformではplanやapply実行時に.tfファイルがすべて読み込まれるようです。
ファイル名やフォルダ名は自由に指定できます。(モジュールの.tfファイルはmain.tfで呼び出された時に読み込まれる)
1つのファイルにすべて記入なども可能ですが、わかりやすいように分けています。
同じモジュールをmain.tfから複数回呼ぶことができるため、設定値が違うリソースを複数作成する場合はモジュールを分けていたほうが便利だったりします。
あくまで例なので、terraform_settings.tfとprovider.tfを一緒にしたり、main.tfに変数定義を含めてvariable.tfを作成しないなどもありだと思います。
#env/dev/terraform_settings.tf
https://www.terraform.io/docs/language/settings/index.html
Terraformのバージョンやプロバイダーのバージョンを指定しています。
stateファイルの保存先も設定します。
terraform {
required_version = "0.14.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.35.0"
}
}
backend "s3" {
bucket = "dev-terraform-state-xxxxxxxx"
key = "sample/terraform.tfstate"
region = "ap-northeast-1"
}
}
#env/dev/provider.tf
https://www.terraform.io/docs/language/providers/requirements.html
プロバイダーの情報を設定しています。
aliasを指定することで複数設定でき、リソースを作成するリージョンを変更する際に使用していました。
provider "aws" {
region = "ap-northeast-1"
profile = "dev"
}
provider "aws" {
alias = "useast1"
region = "us-east-1"
profile = "dev"
}
プロファイルはモジュール呼び出し時やリソース定義などで指定することができます。
module "sample" {
source = "../../module/sample1"
env = var.env
providers = {
aws = aws.useast1
}
}
resourceでも指定できます。
モジュール形式の場合、resourceで指定するとややこしくなるのであまりやらないほうがよさそうです。。
resource "aws_kinesis_firehose_delivery_stream" "firehose_sample" {
provider = aws.useast1
略
データソースでも指定できます。
data "aws_cloudformation_stack" "sample1_stack" {
name = "sample1-fucntion-stack"
provider = aws.useast1
}
#env/dev/data.tf
https://www.terraform.io/docs/language/data-sources/index.html
既存のリソースのarnなどをTerraform定義で使用したい場合に定義します。
取得方法については各リソースのData Sourceのドキュメントを参照してください。
いくつか例を記載します。
# state情報を取得
data "terraform_remote_state" "sample_state" {
backend = "s3"
config = {
bucket = "dev-terraform-state-xxxxxxxx"
key = "sample_state/terraform.tfstate"
region = "ap-northeast-1"
profile = "dev"
}
}
# VPCの情報を取得
data "aws_vpc" "main" {
filter {
name = "tag:Name"
values = ["main_vpc"]
}
}
# サブネットIDs(fileterでワイルドカード「*」が指定できる)
data "aws_subnet_ids" "subnet_ids" {
vpc_id = data.aws_vpc.main.id
filter {
name = "tag:Name"
# アベイラビリティーゾーンの文字識別子を「*」で指定し複数取得している
values = ["dev-sample-ap-northeast-1*"]
}
}
# Lambda関数の情報を取得
data "aws_lambda_function" "sample" {
function_name = "dev-sample-function"
}
# Cloudformationのstackを取得
data "aws_cloudformation_stack" "sample_cloudformation_stack" {
name = "sample_stack"
}
Lambda関数はTerraformで作成するよりもSAM(Serverless Application Model)で作成したほうが楽だったりするので、
SAMで作成したLambda関数をdataで取り込んで使用するなどしていました。
#env/dev/variable.tf
変数を設定しています。設定方法にはVariablesとLocal Valuesがあります。
##Variables
以下のように設定します。(ドキュメントから引用)
variable "image_id" {
type = string
}
variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}
variable "docker_ports" {
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 8300
external = 8300
protocol = "tcp"
}
]
}
resource "aws_instance" "example" {
instance_type = "t2.micro"
ami = var.image_id
}
変数値の指定しつつapply実行する場合、以下のようにします。
$ terraform apply -var="image_id=ami-abc123"
##Local Values
https://www.terraform.io/docs/language/values/locals.html
以下のように設定します。(ドキュメントから引用)
locals {
# Ids for multiple sets of EC2 instances, merged together
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
resource "aws_instance" "example" {
# ...
tags = local.common_tags
}
##variableとlocalsの使い分け
基本はLocal Valuesを使用して、terraform apply -var
で値を変えられるようにしておきたい時にVariablesを使用するのが良さそうです。
参考:https://febc-yamamoto.hatenablog.jp/entry/2018/01/30/185416
#env/dev/main.tf
https://www.terraform.io/docs/language/modules/syntax.html
モジュールとmain.tfについてはひとまとめに以下の例で示します。
mian.tfには作成したモジュールを指定し、必要な変数の値を設定します。
以下、S3を作成するモジュールを作成し、main.tf呼び出している例です。
※terraform_settings.tfなどは省略
sample1
┗env/dev/main.tf
┗module/sample_s3/s3.tf
module "sample" {
# sourceにモジュールのパスを指定する。
source = "../../module/sample_s3"
# モジュールで設定した変数を設定する。
bucket_name = "sample-s3-bucket"
env = "dev"
}
resource "aws_s3_bucket" "s3_bucket" {
bucket = var.bucket_name
acl = "private"
tags = {
env = var.env
}
}
variable "bucket_name" {}
variable "env" {}
#env/dev/output.tf
https://www.terraform.io/docs/language/values/outputs.html
作成したリソースをモジュール外などで使用したい場合に設定します。
以下のようにリソース種別.リソース名で設定します。
以下はenv/dev/main.tfの例で作成したモジュールをoutputした例です。
output "aws_s3_bucket" {
value = aws_s3_bucket.s3_bucket
}
outputした内容は別のモジュール呼び出しで使用する場合は以下のようにします。
module "sample" {
# 略(env/dev/main.tfと同じ)
}
module "sample_use_module_output" {
source = "../../module/sample"
# outputした内容を使用する例
sample_val = module.sample.aws_s3_bucket.arn
}
別state(※この言い方でいいのかは謎)で使用したい場合はさらにoutputします。
output "sample_s3_bucket" {
value = module.sample.aws_s3_bucket
}
別state情報をデータソースで取得して使用します。
# state情報を取得
data "terraform_remote_state" "sample_state" {
backend = "s3"
config = {
bucket = "dev-terraform-state-xxxxxxxx"
key = "sample_state/terraform.tfstate"
region = "ap-northeast-1"
profile = "dev"
}
}
# state情報の内容を使用
module "sample" {
source = "../../module/sample2"
s3_bucket_name = data.terraform_remote_state.sample_state.outputs.sample_s3_bucket.arn
# ~~略~~
}
関数
Terraformでは便利な関数が用意されており、Terraform定義上で使用できます。
以下のドキュメントから参照してみてください。
(左のメニューのFunctionsから各関数の詳細が見れます)
いくつかよく使いそうな関数の例を紹介します。
##file
ファイルを読み込みます。
https://www.terraform.io/docs/language/functions/file.html
file("${path.module}/files/hello.txt")
##templatefile
ファイルを読み込みます。さらにファイル内で変数を定義し、読み込む際に代入できます。
https://www.terraform.io/docs/language/functions/templatefile.html
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "${policy_principal}"
},
"Action": "s3:GetObject",
"Resource": "${bucket_arn}/*"
}
]
}
templatefile(
"${path.module}/files/role_policy.json",
{
bucket_arn = local.bucket_arn
policy_principal = local.policy_principal
}
)
##concat
複数のリストを結合して1つのリストにできます。
https://www.terraform.io/docs/language/functions/concat.html
concat(["a", ""], ["b", "c"])
# 結果
[
"a",
"",
"b",
"c",
]
##flatten
複数のネストされたリストを1つのリストにできます。
https://www.terraform.io/docs/language/functions/flatten.html
flatten([["a", "b"], [], ["c"]])
# 結果
["a", "b", "c"]
flatten([[["a", "b"], []], ["c"]])
# 結果
["a", "b", "c"]
##path.module
file、templatefileで使用しているpath.module
は配置されるモジュールのファイルシステムパスを指します。
https://www.terraform.io/docs/language/expressions/references.html
main.tfでpath.module
を使用している際のファイルの位置関係は以下のようになります。
sample
┗ env/dev
┗ main.tf
┗ files/hello.txt
output.tfで出力してみるとpath.module
には「.
」が入っていました。(他のpathなども一緒に出力してみた)
path_module = "."
path_root = "."
path_cwd = "/home/username/terraform定義を保存しているパス/sample/env/dev"
terraform_workspace = "default"
ダイナミックブロック(Dynamic Blocks)
ダイナミックブロックを使用すると一部リソースタイプなどにある繰り返し指定可能なブロックを、与えられたリストの要素数だけ繰り返し設定できます。
resource, data, provider, provisionerブロックの内部でサポートされています。
(ドキュメントには可読性や保守性が落ちる可能性があると記述されています。なので乱用厳禁)
繰り返し指定可能なブロックは以下のようなブロックです。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elastic_beanstalk_environment#example-with-options
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
application = aws_elastic_beanstalk_application.tftest.name
solution_stack_name = "64bit Amazon Linux 2015.03 v2.0.3 running Go 1.4"
setting {
namespace = "aws:ec2:vpc"
name = "VPCId"
value = "vpc-xxxxxxxx"
}
setting {
namespace = "aws:ec2:vpc"
name = "Subnets"
value = "subnet-xxxxxxxx"
}
}
以下はCloudFrontでLambda@Edgeを設定している例です。
(ちょっと長いので該当箇所のみ抜粋)
resource "aws_cloudfront_distribution" "distribution" {
# ~~略~~
default_cache_behavior {
# ~~略~~
# ダイナミックブロックで複数設定
dynamic "lambda_function_association" {
for_each = var.edgelambda_list
content {
event_type = lambda_function_association.value.event_type
lambda_arn = lambda_function_association.value.lambda_arn
include_body = false
}
}
}
}
module "cloudfront_sample" {
source = "../../module/common/sample"
# ~~略~~
edgelambda_list = [
{
event_type = local.edgelambda_sample1_event_type
lambda_arn = local.edgelambda_sample1_arn
},
{
event_type = local.edgelambda_sample2_event_type
lambda_arn = local.edgelambda_sample2_arn
}
]
}
Lambda@Edgeを複数設定するのに使用しましたが、ダイナミックブロックを使用しなくてもリストで複数設定できるようです。
上記コードはあくまでもダイナミックブロックの使用例として参考にしてください。
lambda_function_associations = [
{
event_type = "viewer-request"
// 注意: lambda_arnはpublishされたものしか使えない
// またlambda_arnをterraformのinterpolation経由で持ってこようとするとうまくいかないのでベタがきしている
lambda_arn = "arn:aws:lambda:us-east-1:${var.account_id}:function:cloudfront_s3_basic_auth_dev:2"
},
]
既存リソースのimport
以下のようにimport用のtfファイルを作成して実行します。
ECSクラスターをimportする例です。
import_sample
┗import.tf # 名前は適当
terraform {
required_version = "0.13.5"
}
provider "aws" {
region = "ap-northeast-1"
profile = "dev"
}
resource "aws_ecs_cluster" "ecs_cluster_sample" {}
# 初期化
$ terraform init -backend-config="profile=dev"
# terraform import リソース種別.名前 import対象(各リソース種別のドキュメントにあるImportを参照)
# ここでは既に作成されているsample_clusterという名前を指定しています。
$ terraform import aws_ecs_cluster.ecs_cluster_sample sample_cluster
terraform import
実行後、terrafrom show
を実行し内容を取得します。
$ terraform show
# aws_ecs_cluster.ecs_cluster_sample:
resource "aws_ecs_cluster" "ecs_cluster_sample" {
arn = "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:cluster/sample_cluster"
capacity_providers = [
"FARGATE",
"FARGATE_SPOT",
]
id = "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:cluster/sample_cluster"
name = "sample_cluster"
tags = {}
tags_all = {}
setting {
name = "containerInsights"
value = "enabled"
}
}
取得した内容をもとに不要な部分(arnやid)などを削除したり、変数を定義します。
resource "aws_ecs_cluster" "ecs_cluster_sample" {
name = var.ecs_cluster_sample_name
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = var.ecs_cluster_sample_name
Env = var.env
Project = var.project
Terraform = "true"
}
}
variable "ecs_cluster_sample_name" {}
上記をTerraform定義を作成したいリソースごとに実施します。
おわり
TerraformよりもTerraform定義化する対象(AWSなど)の勉強が必要という印象が強いですね。。
今回は以上です。