Terraform を運用するについて考える ~第2回 Terraformを始める~

こんにちは、クラウドリードチームのフクナガです。
みなさん「Terraform」使ってますか?
クラウド利用の拡大に伴い、多くの会社でIaC(Infrastructure as Code)を取り入れようと様々な取り組みをされているかと思います。
私も、その中の1人として約3年ほどTerraformを利用した環境構築や運用に携わってきました。
今回の記事では、Terraformをより堅牢に利用するための仕組みとそれを構築するためのCloudFormationテンプレートをご紹介します。
TerraformにおけるCI/CDパイプラインの重要性
Terraformソースコードは、「terraform apply」コマンドを実行することで環境へ適用することが可能です。
本記事では、terraform applyを実行する部分をCI/CD(継続的インテグレーション/継続的デリバリー)として構築/利用する手順をご紹介しているのですが、最初にTerraformにおけるCI/CDの必要性について書いてみます。
皆様は、インフラの環境構築/変更を手動で実施した経験はありますでしょうか。手動で構築/変更する場合は事前にパラメータレベルで設計書や手順書を作って「どのような作業を行うのか」を定めて、チーム内でレビューし、再鑑者をつけて作業することで想定通りの作業をすることを担保すると思います。
では、Terraformを利用する場合はどうでしょうか?
作業者環境でterraform applyを実行する場合、作業者間でソースコードの差分は発生していないでしょうか?現環境にどの状態のソースコードが適用されているか把握していますか?適用前にチーム内でのレビュープロセスは存在するでしょうか?
上記を解決するのが、TerraformのCI/CDだと私は考えています。
作業者環境からterraform applyを禁止(権限で設定)し、ソースコードをGit管理し、特定のブランチでのterraform applyのみ実行可能にする。さらに、Git管理をすることで「プルリクエスト」によってマージすることで発生するコードの差分をチーム内でレビューすることも可能です。
せっかくTerraformを利用しインフラをソースコードで管理できるようになったわけですから、CI/CDを構築し、よりセキュアに利用していきましょう。
Terraform環境構築テンプレートについて
こちらの記事でご紹介している通り、Terraformを利用するためにいくつかAWSリソースを作成する必要があります。
上記リソースをTerraformコードに含めると、初回実行時に必要なリソースが作成されておらずTerraformコマンドの実行ができません。
Terraform利用のためのリソースをCloudFormationを利用し構築することで、Terraformを利用するための仕組みも含めてすべてをコードの管理下に置くことができます。
また、CloudFormationテンプレートを利用することで構築にかかる工数を削減でき、本質である「Terraformコードの構築」に集中することができます。
※構築用CloudFormationテンプレートは記事の最後に掲載いたします。
Terraform適用までの処理の流れ
Terraformコードの開発から環境への適用は以下の流れで実施します。

構築するリソース
S3
tfstateファイルを管理するために利用します。
また、CodePipelineのアーティファクト格納用バケットも作成します。
DynamoDB
tfstateへの同時書き込みを防ぐために利用します。
CodeCommit
Terraformソースコードを管理するために利用します。
EventBridge
CodeCommitの対象ブランチ(main)へのマージを検知し、CodePipelineを実行する。
CodePipeline / CodeBuild
Terraformを環境へ適用するために利用します。
今回は、CodeCommitのTerraform管理リポジトリのmainブランチへのマージをトリガーとして、Terraformを適用します。
IAMユーザ「GitUser」
以下が実行可能な権限を持つIAMユーザです。
・S3/DynamoDB権限
tfstateファイルを管理するリソースに権限を付与することで、Terraformコマンドが実行可能になります。
・terraform apply以外のTerraformコマンドの実行権限
AWSリソースの読み込み権限を付与することで、terraform planが実行可能になります。
・CodeCommit
CodeCommitで操作を実行する権限を付与することで、CodeCommitを利用したコード開発/管理が可能になります。
※「main」リポジトリへの直接のコミットを禁止する。
利用方法の説明
①開発時
1.IAMユーザ「GitUser」から「アクセスキー」と「AWS CodeCommit の HTTPS Git 認証情報」を取得する
※下記画像赤枠のボタンを押下

テンプレートから構築された「GitUser」は読み込み権限のみを持ったユーザです。
terraform init ~ terraform planまでを実行できますが、terraform applyは権限不足で実行できません。
terraform applyはCodePipeline/CodeBuildで実行する想定です。
万が一AWS認証情報が漏れても、リソースへの操作権限を持ちません。
2.「GitUser」のアクセスキー情報を登録する
aws configure ※上記コマンド押下後に、アクセスキー・シークレットキー・リージョンを入力する
3.作成したCodeCommitリポジトリをローカルにクローンする
(1) AWSマネージメントコンソールで「CodeCommit」へ遷移する
(2) 「CodeCommit」でテンプレートから作成したCodeCommitリポジトリを選択する

(3) 対象リポジトリの画面右上「URLのクローン」を押下、プルダウンメニューから「HTTPSのクローン」を選択する
※実施すると、git clone用のURLがコピーされる

(4) ローカルにリポジトリをクローンする
git clone [コピーしたURL]
※コマンド実行後に、ユーザ・パスワードの入力を求められるため、「AWS CodeCommit の HTTPS Git 認証情報」で取得した内容を入力する
4. ローカル開発
git cloneにて取得したディレクトリ配下で開発作業を行う。
[初期構築]
providers.tfを以下の内容で作成する。
S3バケット名はテンプレートによって作成されたS3バケット名を記載する。
terraform {
required_version = ">= 1.1.9"
backend "s3" {
# tfstateファイル管理用バケット
bucket = "[個有名]-tfstate-bucket"
region = "ap-northeast-1"
key = "terraform.tfstate"
encrypt = true
# tfstate競合防止用dynamodb_table
dynamodb_table = "dynamodbTfstate"
}
}
provider "aws" {
region = "ap-northeast-1"
}
※上記は、terraformバージョンを1.1.9で利用する場合
ローカルでソースコードを作成したら、作業ディレクトリへ移動し以下のコマンドを実行、terraform planの結果を確認します。
# Terraformの初期化 $ terraform init # Terraformソースコードのオートフォーマット $ terraform fmt --recursive # Terraformソースコードの文法チェック $ terraform validate # Terraformコード実行時に作成されるリソースを確認 $ terraform plan
CodeCommitでソースコードを管理する観点から、作業者ごとのフォーマット差異により余計な差分が発生することを防ぐ必要があります。
必ず「terraform fmt –recursive」コマンドを実行し、フォーマットの自動修正を行いましょう。
②Terraformの適用
terraform applyコマンドを実行することでリソースを構築することができますが、このテンプレートでは「CodePipeline, CodeBuild」を利用してterraform applyコマンドを実行します。
以下手順でterraform applyを実行することができます。
以下のコマンドでCodeCommitへブランチを作成します。
$ cd [cloneしたファイル配下のディレクトリ] $ git checkout -b [新規作成するブランチ名] $ git add * $ git commit -m "[コミット時のコメントを記載]" $ git push origin [新規作成するブランチ名]
2. 開発断面ブランチからmainブランチへのマージ
(1) AWSコンソールから「CodeCommit」コンソールへ遷移する。
(2) CodeCommitコンソールで対象のリポジトリを選択し、左側メニューから「プルリクエスト」を選択する。

(3) 「新規プルリクエストの作成」を押下し、ターゲットを「main」、ソースを今回作成したブランチに設定し「比較」を押下。

※mainブランチを未作成の場合は、「ブランチ」> 「ブランチの作成」 からmainブランチを作成する
また、mainブランチを「デフォルトブランチ」としておくことを推奨します

(4) 「タイトル」と「説明」を入力し、「プルリクエストを作成」を押下する。
※差分が表示されるため、想定していない差分が発生していないことを確認する。

(5) 作成したプルリクエストを確認し、想定していない変更が存在しないことを確認、「マージ」を押下する。


(6) 「3ウェイマージ」を選択し、自身のユーザ名・メールアドレスを入力、「プルリクエストのマージ」を押下する

3. terraform plan実行結果の確認 / terraform applyの実行
(1) AWSコンソールから「CodePipeline」コンソールへ遷移する。
(2) パイプラインの中から「Terraform-CICD」を選択する。
(3) 「PLAN」が進行中であることを確認し、詳細を確認する。

(4) 処理が完了したら、表示されたログを確認する。

[確認観点]
・想定通りのリソースが構築されるか
・パラメータなどで不備がないか
インスタンスのクラスやリソース名などがミス多めです
・destroyが実行されるリソース
既存システムに影響が出る可能性が高いので、要確認
(5) 内容に問題がなければCodePipeline「Terraform-CICD」の画面へ戻り、「Approval」の「レビュー」を押下する
→上記を実施することで、後続の「APPLY」が実行され、Terraformが適用される。


(6) Terraform適用結果を確認する


まとめ
皆様、CI/CD構築できましたか?
こちらの記事を参考にTerraform適用のCI/CDを構築し、より便利にTerraformを活用いただければ幸いです。
今後は、実際のユースケースに応じたブランチ戦略であったりディレクトリ構成に関するナレッジも投稿しますので、次回記事も見ていただけると嬉しいです。
CloudFormationテンプレート
以下のソースコード内の変数パラメータを適宜変更し、利用してください。
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For Lambda Deploy
Parameters:
CodeCommitRepository:
Type: String
Default: [個有名]-terraform-repository
Description: Terraform Repository
CodePipelineName:
Type: String
Default: Terraform-CICD
Description: CodePipeline Name
BucketNameArtifact:
Type: String
Default: [個有名]-codepipeline-artifact
Description: S3 Name for Artifact
BucketNameTfstate:
Type: String
Default: [個有名]-tfstate-bucket
Description: S3 Name for Artifact
CodeBuildNamePLAN:
Type: String
Default: terraform-plan
Description: Access from CodeBuild
CodeBuildNameAPPLY:
Type: String
Default: terraform-apply
Description: Access from CodeBuild
TargetBranch:
Type: String
Default: main
Description: Target branch Name
TableNameTfstate:
Type: String
Default: dynamodbTfstate
Description: DynamoDB Table Name
TFversion:
Type: String
Default: [利用するTerraformのバージョン]
Description: terraform version
IAMUserName:
Type: String
Default: GitUser
Description: User for codecommit
Resources:
# Terraformコード格納用CodeCommitリポジトリ
CodeCommit:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref CodeCommitRepository
RepositoryDescription: CodeCommit Repository
# CodeCommit変更検知用CloudWatchEvent
AmazonCloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- "CodeCommit Repository State Change"
resources:
- !Join [
"",
[
"arn:aws:codecommit:",
!Ref AWS::Region,
":",
!Ref AWS::AccountId,
":",
!Ref CodeCommitRepository,
],
]
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- !Ref TargetBranch
Targets:
- Arn: !Join
- ""
- - "arn:aws:codepipeline:"
- !Ref AWS::Region
- ":"
- !Ref AWS::AccountId
- ":"
- !Ref CodePipelineName
RoleArn: !GetAtt AmazonCloudWatchEventRole.Arn
Id: codepipeline-AppPipeline
AmazonCloudWatchEventRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- "sts:AssumeRole"
Path: /
Policies:
- PolicyName: cwe-pipeline-execution
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: "codepipeline:StartPipelineExecution"
Resource: !Join
- ""
- - "arn:aws:codepipeline:"
- !Ref AWS::Region
- ":"
- !Ref AWS::AccountId
- ":"
- !Ref CodePipelineName
# terraform build実行用CodeBuild
CodeBuildProjectPLAN:
Type: AWS::CodeBuild::Project
Properties:
Name: !Ref CodeBuildNamePLAN
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: |-
version: 0.2
phases:
install:
commands:
- wget https://releases.hashicorp.com/terraform/${tfversion}/terraform_${tfversion}_linux_amd64.zip
- unzip terraform_${tfversion}_linux_amd64.zip
- mv terraform /usr/bin/
- terraform -v
build:
commands:
- cd ${CODEBUILD_SRC_DIR}/
- terraform init
- terraform validate
- terraform plan
Environment:
Type: LINUX_CONTAINER
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
ComputeType: BUILD_GENERAL1_SMALL
EnvironmentVariables:
- Name: tfversion
Type: PLAINTEXT
Value: !Ref TFversion
ServiceRole: !Ref CodeBuildServiceRole
# terraform apply実行用CodeBuild
CodeBuildProjectAPPLY:
Type: AWS::CodeBuild::Project
Properties:
Name: !Ref CodeBuildNameAPPLY
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: |-
version: 0.2
phases:
install:
commands:
- wget https://releases.hashicorp.com/terraform/${tfversion}/terraform_${tfversion}_linux_amd64.zip
- unzip terraform_${tfversion}_linux_amd64.zip
- mv terraform /usr/bin/
- terraform -v
build:
commands:
- cd ${CODEBUILD_SRC_DIR}/
- terraform init
- terraform validate
- terraform apply -auto-approve -no-color
Environment:
Type: LINUX_CONTAINER
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
ComputeType: BUILD_GENERAL1_SMALL
EnvironmentVariables:
- Name: tfversion
Type: PLAINTEXT
Value: !Ref TFversion
ServiceRole: !Ref CodeBuildServiceRole
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
Policies:
- PolicyName: CodeBuildAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Resource: "*"
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Effect: Allow
Resource: "arn:aws:s3:::*"
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
- Effect: Allow
Resource: !Join
- ""
- - "arn:aws:codebuild:"
- !Ref AWS::Region
- ":"
- !Ref AWS::AccountId
- ":report-group/"
- !Ref CodeBuildNamePLAN
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
- Effect: Allow
Resource: !Join
- ""
- - "arn:aws:codebuild:"
- !Ref AWS::Region
- ":"
- !Ref AWS::AccountId
- ":report-group/"
- !Ref CodeBuildNameAPPLY
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
- Effect: Allow
Resource: "*"
Action:
- iam:*
# Terraform実行用CodePipeline
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Ref CodePipelineName
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: !Ref CodeCommitRepository
PollForSourceChanges: false
BranchName: !Ref TargetBranch
RunOrder: 1
OutputArtifacts:
- Name: BuildArtifact
- Name: PLAN
Actions:
- Name: PLAN
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProjectPLAN
RunOrder: 1
InputArtifacts:
- Name: BuildArtifact
- Name: Approval
Actions:
- Name: Approval
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Version: 1
- Name: APPLY
Actions:
- Name: APPLY
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProjectAPPLY
InputArtifacts:
- Name: BuildArtifact
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: PipelinePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Resource:
- !Sub arn:aws:s3:::${ArtifactBucket}/*
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Effect: Allow
Resource: "*"
Action:
- cloudformation:*
- codecommit:*
- codedeploy:*
- codebuild:*
- s3:*
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketNameArtifact
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
# tfstate管理用S3バケット
tfstateBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketNameTfstate
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
# tfstate同時書き込み防止用テーブル
tfstateDynamoDB:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref TableNameTfstate
AttributeDefinitions:
- AttributeName: "LockID"
AttributeType: "S"
KeySchema:
- AttributeName: "LockID"
KeyType: "HASH"
ProvisionedThroughput:
ReadCapacityUnits: "1"
WriteCapacityUnits: "1"
# Terraformソースコード管理用CodeCommitを利用するためのIAMユーザ
IAMUserForGit:
Type: AWS::IAM::User
Properties:
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeCommitFullAccess
- arn:aws:iam::aws:policy/ReadOnlyAccess
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Deny
Action:
- codecommit:GitPush
- codecommit:DeleteBranch
- codecommit:PutFile
- codecommit:CreateCommit
- codecommit:MergeBranchesByFastForward
- codecommit:MergeBranchesBySquash
- codecommit:MergeBranchesByThreeWay
- codecommit:MergePullRequestByFastForward
- codecommit:MergePullRequestBySquash
Resource: "*"
Condition:
StringEqualsIfExists:
codecommit:References:
- refs/heads/main
"Null":
codecommit:References:
- false
PolicyName: DenyCommit
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
Resource: !GetAtt tfstateDynamoDB.Arn
PolicyName: tfstateWrite
UserName: !Ref IAMUserName
Outputs:
CodeCommitRepository:
Description: CodeCommit Name
Value: !Ref CodeCommitRepository
Export:
Name: CodeCommitRepository
テックブログ新着情報のほか、AWSやGoogle Cloudに関するお役立ち情報を配信中!
Follow @twitter2025 Japan AWS Ambassadors / Google Cloud Partner Top Engineer 2025 / 2024 Japan AWS Top Engineers 選出されました! 生成 AI 多めで発信していますが、CI/CDやIaCへの関心も高いです。休日はベースを弾いてます。
Recommends
こちらもおすすめ
-
AWS CodePipelineのエラーを検知し、Backlog自動起票する手順
2023.12.20
-
Terraform AWS Moduleのすすめ
2022.5.25
Special Topics
注目記事はこちら
データ分析入門
これから始めるBigQuery基礎知識
2024.02.28

AWSの料金が 10 %割引になる!
『AWSの請求代行リセールサービス』
2024.07.16