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

AWS

2022.10.26

Topics

こんにちは、クラウドリードチームのフクナガです。
みなさん「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 を運用するについて考える ~第1回 Terraformを管理する~


上記リソースを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中心に気になったトピックを発信していきます!

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら