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 @twitterインフラエンジニア歴5年のフクナガです。2024 Japan AWS Top Engineers選出されました! 生成 AI 多めで発信していますが、CI/CDやIaCへの関心も高いです。休日はベースを弾いてます。
Recommends
こちらもおすすめ
-
AWSサービスをTerraformでコード化する:AWS Chatbot編
2024.3.11
Special Topics
注目記事はこちら
データ分析入門
これから始めるBigQuery基礎知識
2024.02.28
AWSの料金が 10 %割引になる!
『AWSの請求代行リセールサービス』
2024.07.16