AWS RAMを使ったAWS CodeConnections共有をTerraformで自動化する

# はじめに
AWS CodeConnectionsシリーズ第2弾。
過去の記事では、GitLab Self Managedとの接続をAWS CodeConnectionsで行ったが、今回はGitHub(非Enterprise)との接続を行う。

さらに、2025年3月6日には、AWS Resource Access Manager(RAM)を使った簡単なクロスアカウントのAWS CodeConnections共有ができるようになったことで、従来、環境が増えるたびにマネージメントコンソール画面を使ってリソースの作成を完了させる必要があったが、管理アカウントのみの対応で済むようになった。
今回の記事では、以下の構成図の通り、この新しいリソース共有方法も試してみることにする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/227c2321-ce90-4774-82ad-616a3bd8b0eb.png)

関連のリンクはこちら

https://aws.amazon.com/about-aws/whats-new/2025/03/sharing-connections-aws-codeconnections/

また、過去のAWS CodeConnections関連記事は以下を参照していただきたい。

https://qiita.com/neruneruo/items/b570fb6f739f1efb8250

https://qiita.com/neruneruo/items/1535776e5cab5df115bd

なお、本記事を読むにあたっての前提知識は以下の2点である。

- AWS CodePipelineとAWS CodeConnectionsを触ったことがある
- 初歩的なTerraformを読んで内容を理解することができる

# GitHubに接続するAWS CodeConnections
AWS CodeConnectionsでGitHubに接続するには、以下の通り```aws_codeconnections_connection```のみ定義すれば良い。GitLab Self Managedで使っていた```aws_codeconnections_host```については必要ない。

```HCL
resource "aws_codeconnections_connection" "example" {
  name          = local.codeconnections_connection_name
  provider_type = "GitHub"
}
```

このリソースを作った直後は、まだ接続が完了していないためすぐには使えない。
AWSマネージメントコンソールで作成したリソースを選択し、AWS公式ユーザーガイドの

> 2\. AWS コネクタの承認 GitHubを選択します。接続ページが表示され、GitHub アプリフィールドが表示されます。

以降の作業を行う。この承認行為のみ、AWSの公開APIが提供されていないため、今回のTerraformによる自動化の範囲外となる。

https://docs.aws.amazon.com/ja_jp/dtconsole/latest/userguide/connections-create-github.html

また、以下の項ではクロスアカウント対象のアカウントが必要になってくるので、以下のデータソースも宣言しておこう。

```HCL
provider "aws" {
  alias   = "crossaccount"
  profile = "crossaccount"
  region  = "ap-northeast-1"
}


data "aws_caller_identity" "crossaccount" {
  provider = aws.crossaccount
}
```

# AWS Resource Access Managerの設定
## 管理アカウント側の設定
ここはそんなに難しい部分はない。Terraformの公式ドキュメントとほぼ同じ内容になるはずで、```aws_ram_resource_share```を作成し、アクセス元アカウントのプリンシパルについて```aws_ram_principal_association```と連携させ、```aws_ram_resource_association```もろとも紐づけるイメージだ。


:::note info
共有先のアカウントを自分のOrganizations外にする場合は、```allow_external_principals = true```に設定する必要があるので注意。
:::

```HCL
resource "aws_ram_resource_share" "example" {
  name                      = local.ram_share_name
  allow_external_principals = true
}

resource "aws_ram_resource_association" "example" {
  resource_share_arn = aws_ram_resource_share.example.arn
  resource_arn       = aws_codeconnections_connection.example.id
}

resource "aws_ram_principal_association" "example" {
  resource_share_arn = aws_ram_resource_share.example.arn
  principal          = data.aws_caller_identity.crossaccount.account_id
}
```

## クロスアカウント側の設定
管理アカウントアカウント以外に、クロスアカウントでも接続の招待を受けて承認できるようにしておく必要があるため、以下の```aws_ram_resource_share_accepter```を行う。ここはマネージメントコンソール不要で、TerraformのみでスムースにApplyをすることができる。

```HCL
resource "aws_ram_resource_share_accepter" "example" {
  provider = aws.crossaccount

  share_arn = aws_ram_principal_association.example.resource_share_arn
}
```

# AWS CodePipelineでの利用方法
AWS RAMの設定が完了したら、あとはクロスアカウント対象のアカウントのリソースを作っていこう。

## AWS CodePipeline周辺リソース
### Amazon CloudWatch Logs
AWS CodeBuildのデバッグ用に作っておく。

```HCL
resource "aws_cloudwatch_log_group" "codebuild_crossaccount" {
  provider = aws.crossaccount

  name = local.clouddwatch_loggroup_name
}
```

### Amazon S3
Amazon S3は普通にAWS CodePipelineからアクセス可能なPrivateな箱を作っておけばよい。

```HCL
resource "aws_s3_bucket" "example_crossaccount" {
  provider = aws.crossaccount

  bucket = local.s3_bucket_crossaccount_name

  force_destroy = true
}

resource "aws_s3_bucket_ownership_controls" "example_crossaccount" {
  provider = aws.crossaccount

  bucket = aws_s3_bucket.example_crossaccount.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "example_crossaccount" {
  provider = aws.crossaccount

  bucket = aws_s3_bucket.example_crossaccount.id

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

### IAM
IAMは、AWS CodePipelineとAWS CodeBuildについて作成する。
ポイントは```codestar-connections:UseConnection```で管理アカウントのリソースを設定する部分だ。
それ以外は特に難しいところはない。

```HCL
################################################################################
# IAM for AWS CodeBuild                                                        #
################################################################################
resource "aws_iam_role" "codebuild_crossaccount" {
  provider = aws.crossaccount

  name               = local.iam_codebuild_role_name
  assume_role_policy = data.aws_iam_policy_document.codebuild_crossaccount_assume.json
}

data "aws_iam_policy_document" "codebuild_crossaccount_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

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

resource "aws_iam_role_policy" "codebuild_crossaccount" {
  provider = aws.crossaccount

  name   = local.iam_codebuild_policy_name
  role   = aws_iam_role.codebuild_crossaccount.id
  policy = data.aws_iam_policy_document.codebuild_crossaccount_custom.json
}

data "aws_iam_policy_document" "codebuild_crossaccount_custom" {
  statement {
    effect = "Allow"

    actions = [
      "codeBuild:StartBuild",
    ]

    resources = [
      aws_codebuild_project.example_crossaccount.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "s3:GetObject",
    ]

    resources = [
      "${aws_s3_bucket.example_crossaccount.arn}/*",
    ]
  }
  statement {
    effect = "Allow"

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

    resources = [
      aws_cloudwatch_log_group.codebuild_crossaccount.arn,
      "${aws_cloudwatch_log_group.codebuild_crossaccount.arn}:log-stream:*",
    ]
  }
}

################################################################################
# IAM for AWS CodePipeline                                                     #
################################################################################
resource "aws_iam_role" "codepipeline_crossaccount" {
  provider = aws.crossaccount

  name               = local.iam_codepipeline_role_name
  assume_role_policy = data.aws_iam_policy_document.codepipeline_crossaccount_assume.json
}

data "aws_iam_policy_document" "codepipeline_crossaccount_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

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

resource "aws_iam_role_policy" "codepipeline_crossaccount" {
  provider = aws.crossaccount

  name   = local.iam_codepipeline_policy_name
  role   = aws_iam_role.codepipeline_crossaccount.id
  policy = data.aws_iam_policy_document.codepipeline_crossaccount_custom.json
}

data "aws_iam_policy_document" "codepipeline_crossaccount_custom" {
  statement {
    effect = "Allow"

    actions = [
      "codebuild:StartBuild",
      "codebuild:StopBuild",
      "codebuild:BatchGet*",
      "codebuild:Get*",
      "codebuild:List*",
      "s3:*",
    ]

    resources = [
      "*",
    ]
  }

  statement {
    sid = "AllowCodeStarConnections"

    effect = "Allow"

    actions = [
      "codestar-connections:UseConnection",
    ]

    resources = [
      aws_codeconnections_connection.example.arn,
    ]
  }
}
```

### AWS CodeBuild
```HCL
################################################################################
# AWS CodeBuild                                                                #
################################################################################
resource "aws_codebuild_project" "example_crossaccount" {
  provider = aws.crossaccount

  name         = local.codebuild_project_name
  service_role = aws_iam_role.codebuild_crossaccount.arn

  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec.yml"
  }

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    type            = "LINUX_CONTAINER"
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
  }

  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.codebuild_crossaccount.name
    }
  }
}
```

## AWS CodePipeline本体
いよいよAWS CodePipeline本体の作成だ。

AWS CodePipelineでAWS CodeConnectionsを利用する方法は、GitLab Self Managed利用時と大差ない。
```trigger.provider_type```を```CodeStarSourceConnection```に設定するのと、```stage.action.configuration.ConnectionArn```を先に作成しているAWS CodeConnectionの接続のARNを設定しけばよい。

```HCL
resource "aws_codepipeline" "example_crossaccount" {
  provider = aws.crossaccount

  name     = local.codepipeline_pipeline_name
  role_arn = aws_iam_role.codepipeline_crossaccount.arn

  pipeline_type = "V2"

  trigger {
    provider_type = "CodeStarSourceConnection"
    git_configuration {
      source_action_name = "Source"
      push {
        branches {
          includes = ["main"]
        }
      }
    }
  }

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

  stage {
    name = "Source"

    action {
      run_order        = 1
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]
      namespace        = "SourceVariables"

      configuration = {
        ConnectionArn    = aws_codeconnections_connection.example.arn
        FullRepositoryId = "${var.github_name}/example"
        BranchName       = "staging"
      }
    }
  }

  stage {
    name = "Build"

    action {
      run_order       = 2
      name            = "Build"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["SourceArtifact"]

      configuration = {
        ProjectName = aws_codebuild_project.example_crossaccount.name
        EnvironmentVariables = jsonencode([
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_AUTHORDATE"
            value = "#{SourceVariables.AuthorDate}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_BRANCHNAME"
            value = "#{SourceVariables.BranchName}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_COMMITID"
            value = "#{SourceVariables.CommitId}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_COMMITMESSAGE"
            value = "#{SourceVariables.CommitMessage}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_CONNECTIONARN"
            value = "#{SourceVariables.ConnectionArn}"
          },
          {
            type  = "PLAINTEXT"
            name  = "CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME"
            value = "#{SourceVariables.FullRepositoryName}"
          },
        ])
      }
    }
  }
}
```

ビルドでは、各種環境変数をGitHubが設定してくれているので、それぞれ内容を確認してみる。

なお、今回網羅はしたので改めて確認する必要はないが、利用可能な環境変数の一覧については、ユーザーガイドに記載がある。「CodeStarSourceConnection アクションの出力変数」を確認していただけば良い。

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/reference-variables.html#reference-variables-list

これらの環境変数を参照するために、Buildspecは以下のように作成してみよう。

```buildspec.yml
version: 0.2

phases:
  build:
    commands:
      - echo ${CODEPIPELINE_SRCVAR_AUTHORDATE}
      - echo ${CODEPIPELINE_SRCVAR_BRANCHNAME}
      - echo ${CODEPIPELINE_SRCVAR_COMMITID}
      - echo ${CODEPIPELINE_SRCVAR_COMMITMESSAGE}
      - echo ${CODEPIPELINE_SRCVAR_CONNECTIONARN}
      - echo ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME}
```

上記Buildspecメインブランチを更新すると、ビルドログに環境変数が出力される。
上記した環境変数設定が設定され、上手く機能している証拠だ。

```
[Container] 2025/03/09 11:12:40.650869 Running command echo ${CODEPIPELINE_SRCVAR_AUTHORDATE}
2025-03-09T11:12:16Z

[Container] 2025/03/09 11:12:40.656947 Running command echo ${CODEPIPELINE_SRCVAR_BRANCHNAME}
main

[Container] 2025/03/09 11:12:40.662338 Running command echo ${CODEPIPELINE_SRCVAR_COMMITID}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

[Container] 2025/03/09 11:12:40.667594 Running command echo ${CODEPIPELINE_SRCVAR_COMMITMESSAGE}
Initial commit.

[Container] 2025/03/09 11:12:40.672782 Running command echo ${CODEPIPELINE_SRCVAR_CONNECTIONARN}
arn:aws:codeconnections:ap-northeast-1:XXXXXXXXXXXX:connection/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

[Container] 2025/03/09 11:12:40.677835 Running command echo ${CODEPIPELINE_SRCVAR_FULLREPOSITORYNAME}
neruneruo/example
```

これで、AWS CodeConnectionsを用いた接続方法の選択肢が増えた、
パイプラインのソースにGitHubを使って、より良い開発環境を整えていこう！