# [Terraform]
CodePipeline で AppConfig へデプロイする


published: 2025-01-23

AWS AppConfig へデプロイしてみた。


## AppConfig

AppConfig には 設定プロファイル (Configuration Profile) と 環境 (Environment) という2つの概念があります。

・設定プロファイル ... 設定データのソースやバリデーションなど
・環境 ... デプロイ先

本記事では、設定プロファイルにて CodePipeline をソースとして指定します。
そして環境へのデプロイを試みます。


![appconfig-configuration-profile.png](https://lab.enuesaa.dev/prototype/aws-codepipeline-appconfig-terraform/appconfig-configuration-profile.png)

## 構成

CodePipeline を実行すると AppConfig の環境へデプロイされます。

1. git push で CodePipeline を実行
2. CodeBuild でビルド
3. AppConfig へデプロイ


![architecture.png](https://lab.enuesaa.dev/prototype/aws-codepipeline-appconfig-terraform/architecture.png)

## コード

### `codebuild_buildspec.yml`

```yml
version: 0.2

phases:
  build:
    commands:
      - echo '{"time":"'"$(date -Iseconds)"'"}' > config.json

artifacts:
  files:
    - '**/*'

```

### `codebuild_logs.tf`

```tf
resource "aws_cloudwatch_log_group" "codebuild" {
  name = "/aws/codebuild/${var.identifier}"

  retention_in_days = 30
}

```

### `codebuild_role.tf`

```tf
resource "aws_iam_role" "codebuild" {
  name = "${var.identifier}-codebuild"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "codebuild.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "codebuild_s3" {
  name = "s3"

  role = aws_iam_role.codebuild.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "s3:GetObject",
          "s3:PutObject",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${aws_s3_bucket.codepipeline_artifact.bucket}/*"
      },
    ]
  })
}

resource "aws_iam_role_policy" "codebuild_logs" {
  name = "logs"

  role = aws_iam_role.codebuild.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

```

### `codebuild.tf`

```tf
resource "aws_codebuild_project" "main" {
  name = var.identifier

  service_role = aws_iam_role.codebuild.arn

  source {
    type      = "CODEPIPELINE"
    buildspec = file("${path.module}/codebuild_buildspec.yml")
  }

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/standard:6.0"
    type         = "LINUX_CONTAINER"
  }

  logs_config {
    cloudwatch_logs {
      group_name  = aws_cloudwatch_log_group.codebuild.name
    }
  }
}

```

### `codepipeline_artifact.tf`

```tf
resource "aws_s3_bucket" "codepipeline_artifact" {
  bucket = "${var.identifier}-codepipeline-artifact"

  force_destroy = true
}

resource "aws_s3_bucket_ownership_controls" "codepipeline_artifact" {
  bucket = aws_s3_bucket.codepipeline_artifact.bucket

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "codepipeline_artifact" {
  bucket = aws_s3_bucket.codepipeline_artifact.bucket

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

```

### `codepipeline_role.tf`

```tf
resource "aws_iam_role" "codepipeline" {
  name = "${var.identifier}-codepipeline"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "codepipeline.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "codepipeline_s3" {
  name = "s3"

  role = aws_iam_role.codepipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "s3:GetObject",
          "s3:PutObject",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${aws_s3_bucket.codepipeline_artifact.bucket}/*"
      },
    ]
  })
}

resource "aws_iam_role_policy" "codepipeline_codeconnection" {
  name = "codeconnection"

  role = aws_iam_role.codepipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "codeconnections:UseConnection",
          "codestar-connections:UseConnection",
        ]
        Effect   = "Allow"
        Resource = var.connection_arn
      },
    ]
  })
}

resource "aws_iam_role_policy" "codepipeline_codebuild" {
  name = "codebuild"

  role = aws_iam_role.codepipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "codebuild:BatchGetBuilds",
          "codebuild:StartBuild",
          "codebuild:BatchGetBuildBatches",
          "codebuild:StartBuildBatch",
        ]
        Effect   = "Allow"
        Resource = aws_codebuild_project.main.arn
      },
    ]
  })
}

resource "aws_iam_role_policy" "codepipeline_appconfig" {
  name = "appconfig"

  role = aws_iam_role.codepipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "appconfig:StartDeployment",
          "appconfig:StopDeployment",
          "appconfig:ListApplications",
          "appconfig:ListConfigurationProfiles",
          "appconfig:GetConfiguration",
          "appconfig:GetDeployment",
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

```

### `codepipeline.tf`

```tf
resource "aws_codepipeline" "main" {
  name = var.identifier

  pipeline_type = "V2"
  role_arn = aws_iam_role.codepipeline.arn

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

  stage {
    name = "Source"

    action {
      name            = "Source"
      category        = "Source"
      owner           = "AWS"
      provider        = "CodeStarSourceConnection"
      version         = "1"
      output_artifacts = ["SourceOutput"]

      configuration = {
        ConnectionArn = var.connection_arn
        FullRepositoryId = var.repository
        BranchName = var.branch
      }
    }
  }

  stage {
    name = "Build"

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

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

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "AppConfig"
      version         = "1"
      input_artifacts  = ["BuildOutput"]

      configuration = {
        Application = aws_appconfig_application.main.id
        DeploymentStrategy = "AppConfig.Linear50PercentEvery30Seconds"
        Environment = aws_appconfig_environment.main.environment_id
        ConfigurationProfile = aws_appconfig_configuration_profile.main.configuration_profile_id
        InputArtifactConfigurationPath = "config.json"
      }
    }
  }
}

```

### `main.tf`

```tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      configuration_aliases = [ aws, aws.virginia ]
    }
  }
}

data "aws_caller_identity" "main" {}
data "aws_region" "main" {}

```

### `variables.tf`

```tf
variable "identifier" {
  type = string
}

# codepipeline
variable "connection_arn" {
  type = string
}

variable "repository" {
  type = string
}

variable "branch" {
  type = string
}

```

### `appconfig.tf`

```tf
resource "aws_appconfig_application" "main" {
  name = var.identifier
}

resource "aws_appconfig_environment" "main" {
  application_id = aws_appconfig_application.main.id
  name = "dev"
}

resource "aws_appconfig_configuration_profile" "main" {
  application_id = aws_appconfig_application.main.id
  name = "main"

  # see https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-configurationprofile.html#cfn-appconfig-configurationprofile-locationuri
  location_uri = "codepipeline://${var.identifier}"
}

```

### memos

AppConfig

mark: `appconfig.tf:1`

location_uri のフォーマットは CloudFormation のドキュメントを参照。


mark: `appconfig.tf:15`

- [CloudFormation ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-configurationprofile.html#cfn-appconfig-configurationprofile-locationuri)
- [aws_appconfig_configuration_profile](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appconfig_configuration_profile)
CodeBuild

mark: `codebuild.tf:1`

サンプルとして config.json を作成。
これを AppConfig に保存する。


mark: `codebuild_buildspec.yml:6`

CodePipeline

mark: `codepipeline.tf:1`

AppConfig をデプロイするためのIAM権限が必要

mark: `codepipeline_role.tf:80`

## Links

- [https://aws.amazon.com/jp/blogs/news/automating-feature-release-using-aws-appconfig-integration-with-aws-codepipeline/](https://aws.amazon.com/jp/blogs/news/automating-feature-release-using-aws-appconfig-integration-with-aws-codepipeline/)
