Terraform + CodePipeline + CodeBuild + S3 で Cloudfront に配信する

7 min読了の目安(約6900字TECH技術記事

みなさん、こんにちは!めもりー(@m3m0r7) です。
Terraform を初めて 2 週間ほどのインフラ初心者です。

主にやりたいこととしては下記のようになります。

そのために S3 に展開する設定を含めて Terraform でどう書けばよいのかしたためてみます。
CodeCommit にプッシュしてビルドして Lambda で配信するといった用途なら多く見かけたのですが、残念ながらこういった用途はあまり見かけないので少し苦戦していました。Terraform 難しい。

書いていく

CodePipeline で一連の流れを定義するにあたり、下記が必要になります。

  1. CodePipeline のためのロールの設定
  2. CodeBuild のためのロールの設定
  3. ロールをそれぞれ紐付ける
  4. CodeBuild のプロジェクトの設定
  5. CodePipeline のアプリケーションの設定

それぞれ見ていきます。

1. CodePipeline のためのロールの設定

CodePipeline で一体どういったロールを設定すればいいのか正直難しいところもあり beyond さんの Github + CodeBuild + CodePipelineを利用したFargateのデプロイフローをTerraformで構築する という記事を参考にさせていただきました。

当該の記事でデプロイに不要そうな情報をいくつか取り除き data で以下のように IAM ポリシーを定義し、それを使用します。

data "aws_iam_policy_document" "codepipeline_policy" {
  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "codedeploy:CreateDeployment",
      "codedeploy:GetApplication",
      "codedeploy:GetApplicationRevision",
      "codedeploy:GetDeployment",
      "codedeploy:GetDeploymentConfig",
      "codedeploy:RegisterApplicationRevision"
    ]

    resources = ["*"]
  }

  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "elasticbeanstalk:*",
      "ec2:*",
      "elasticloadbalancing:*",
      "autoscaling:*",
      "cloudwatch:*",
      "s3:*",
      "sns:*",
      "cloudformation:*",
      "rds:*",
      "sqs:*",
      "ecs:*"
    ]

    resources = ["*"]
  }

  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "codebuild:BatchGetBuilds",
      "codebuild:StartBuild"
    ]

    resources = ["*"]
  }

}

権限に関しては、こんなにいらないので、もう少し削っても良い気はしています。
次に AssumeRole の定義を下記のように行います。

data "aws_iam_policy_document" "codepipeline_assume_policy" {
  statement {
    sid    = ""
    effect = "Allow"

    principals {
      identifiers = [
        "codepipeline.amazonaws.com",
      ]
      type = "Service"
    }

    actions = [
      "sts:AssumeRole",
    ]
  }
}

2. CodeBuild のためのロールの設定

CodePipeline 同様に CodeBuild のための IAM ポリシーと AssumeRole を定義します。

data "aws_iam_policy_document" "codebuild_policy" {
  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["*"]
  }

  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "ec2:CreateNetworkInterface",
      "ec2:DescribeDhcpOptions",
      "ec2:DescribeNetworkInterfaces",
      "ec2:DeleteNetworkInterface",
      "ec2:DescribeSubnets",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeVpcs"
    ]

    resources = ["*"]
  }

  statement {
    sid    = ""
    effect = "Allow"

    actions = [
      "s3:*"
    ]

    resources = ["*"]
  }
}
data "aws_iam_policy_document" "codebuild_assume_policy" {
  statement {
    sid    = ""
    effect = "Allow"

    principals {
      identifiers = [
        "codebuild.amazonaws.com",
      ]
      type = "Service"
    }

    actions = [
      "sts:AssumeRole",
    ]
  }
}

3. ロールをそれぞれ紐付ける

data で定義したロールをそれぞれ下記のように紐付けます。
var.env は予め、指定した現在の環境、例えば stg, prd などが代入される想定です。

Terraform を触っていて、私がものすごく分かりづらかったのが変数の使用である var.*** でした。
余談ですが、この var は variable ブロックで定義した値が入ります。これらの値は terraform.tfvars などで定義したり、環境変数で定義した値が入っていきます。
階層が深くなるにつれて、それをモジュールに対してプロパゲーションしていく必要があります。
Terraform の定義を Go で書ければものすごい楽なんだけどなと何回か思ったのは秘密です。

さて、下記のように IAM ポリシーを定義することでロールとポリシーの設定ができるようになります。

resource "aws_iam_role" "codepipeline_role" {
  name = "${var.env}-codepipeline-role"
  assume_role_policy = data.aws_iam_policy_document.codepipeline_assume_policy.json
}

resource "aws_iam_role_policy" "codepipeline_role_policy" {
  name = "${var.env}-codepipeline-policy"
  role = aws_iam_role.codepipeline_role.id
  policy = data.aws_iam_policy_document.codepipeline_policy.json
}

resource "aws_iam_role" "codebuild_role" {
  name = "${var.env}-codebuild-role"
  assume_role_policy = data.aws_iam_policy_document.codebuild_assume_policy.json
}

resource "aws_iam_role_policy" "codebuild_policy" {
  role = aws_iam_role.codebuild_role.name
  policy = data.aws_iam_policy_document.codebuild_policy.json
}

4. CodeBuild のプロジェクトの設定

CodePipeline で使用するための CodeBuild のプロジェクトを設定します。

CodeBuild の定義は下記のようになります。

resource "aws_codebuild_project" "frontend" {
  name          = "${var.env}-codebuild-frontend"
  description   = "build frontend for ${var.env}"
  build_timeout = "60"
  service_role  = aws_iam_role.codebuild_role.arn

  artifacts {
    type                        = "CODEPIPELINE"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:3.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = false
  }

  source {
    type            = "CODEPIPELINE"
  }
}

5. CodePipeline のアプリケーションの設定

次に CodePipeline の設定です。


resource "aws_codepipeline" "frontend" {
  name = "${var.env}-codepipeline-frontend"
  role_arn = aws_iam_role.codepipeline_role.arn

  artifact_store {
    location = aws_s3_bucket.deploy_bucket.bucket
    type = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "S3"
      version          = "1"
      output_artifacts = ["source"]

      configuration = {
        S3Bucket    = aws_s3_bucket.deploy_bucket.bucket
        S3ObjectKey = "frontend.zip"
        PollForSourceChanges = true
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source"]
      output_artifacts = ["build"]
      version          = "1"

      configuration = {
        ProjectName = aws_codebuild_project.frontend.name
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name             = "Deploy"
      category         = "Deploy"
      owner            = "AWS"
      provider         = "S3"
      version          = "1"
      input_artifacts = ["build"]

      configuration = {
        BucketName    = var.storage.cloudfront_bucket.bucket
        Extract = true
      }
    }
  }
}

S3 にプッシュするときは aws_s3_bucket.deploy_bucket.bucket で指定しているバケットに frontend.zip という名前で下記のようにプッシュします。

aws s3 cp \
  frontend.zip \
  s3://<< バケット名 >>/frontend.zip

そうするとポーリングを行っている CodePipeline で自動的に処理が始まるようになります。

またステージの設定における Deploy は Extracttrue にすることで指定したバケットに対してそのままファイル全てを展開することができるようになります。

var.storage.cloudfront_bucket.bucket は Cloudfront に指定しているバケットの名前を指定します。

これで一連の処理ができるようになりました。