先輩がノリで作ったAWSをちゃんとコード化してみた2 (CodePipelineのコード化)

## コード化中
1. [AWS CDKの導入](https://qiita.com/emi_yoshi/items/d775cd80e44095d468f1)
1. CodePipelineのコード化(この記事)
1. [EventBridge Schedulerのコード化](https://qiita.com/emi_yoshi/items/f19044eb0ac843e6cfca)
1. [ECS clusterとserviceのコード化](https://qiita.com/emi_yoshi/items/62311276cb84ca7e4db6)
1. [ElastiCacheのコード化、本番環境構築](https://qiita.com/emi_yoshi/items/93913cca44658067cd98)

## CodePipeline
- 自分自身反映用
- バッチ処理のリポジトリのコード反映用(Source, Build)
- API用のリポジトリのコード反映用(Source, Build, Deploy)
- 管理画面用のリポジトリのコード反映用(Source, Build, Deploy)

の4つ作成します

CDKで作成するCodePipelineは2種類あって
- 自分自身を更新する場合は、software.amazon.awscdk.pipelines.CodePipeline
- 全く別のCodePipelineは、software.amazon.awscdk.services.codepipeline.Pipeline

を使用します

## 自分自身を更新するCodePipeline

[このワークショップ](https://cdkworkshop.com/ja/50-java/70-advanced-topics/100-pipelines/1000-setting-up.html)が自分自身を更新する用のCodePipelineを作成していて、今回対応したいことと同じなので参考にしていきます

ゴールは、インフラ用のGithubリポジトリのdevelopブランチにpushされたらインフラの変更が反映されることです

### ソース
ワークショップはCodeCommitのリポジトリを使用していますが、Githubのリポジトリにしたいので
```Kotlin
val context = this.getNode().tryGetContext("development") as Map<String, Any>

val connectionArn = context["infraConnectionArn"].toString()
val owner = context["githubOwner"].toString()
val repository = context["infraRepository"].toString()
val branch = context["infraBranch"].toString()

val github: CodePipelineSource = CodePipelineSource
    .connection(
        owner + "/" + repository,
        branch,
        ConnectionSourceOptions.builder()
            .connectionArn(connectionArn)
            .build()
    )
```

cdk.jsonはこんな感じで環境ごとに分けるようにしています

```Kotlin:cdk.json
{
  "app": "/bin/bash ./app.sh",
  "context": {
    "awsAccountId": "awsAccountId",
    "awsDefaultRegion": "ap-northeast-1",

    "development": {
      "env": "development",
      "githubOwner": "githubOwner",
      "infraConnectionArn": "arn:aws:codestar-connections:ap-northeast-1:awsAccountId:connection/infraConnectionArn",
      "infraRepository": "infraRepository",
      "infraBranch": "develop",
    }
    "production": {
      "env": "production",
      "githubOwner": "githubOwner",
      "infraConnectionArn": "arn:aws:codestar-connections:ap-northeast-1:awsAccountId:connection/infraConnectionArn",
      "infraRepository": "infraRepository",
      "infraBranch": "main",
    }
  }
}
```

### CodePipelineをcreate
ディレクトリ階層を変えたのでprimaryOutputDirectoryの指定が必要でした

```Kotlin
val pipeline: CodePipeline = CodePipeline.Builder
    .create(this, "MyappInfraCdkPipeline")
    .pipelineName("MyappInfraCdkPipeline")
    .synth(
        CodeBuildStep.Builder
            .create("Synth")
            .input(github)
            .installCommands(listOf(
                "npm install -g aws-cdk"
            ))
            .commands(listOf(
                "cd infra",
                "npx cdk synth"
            ))
            .primaryOutputDirectory("infra/cdk.out")
            .build()
    )
    .build()
```

### stageの追加
```Kotlin
val deployStage: Stage = DeployStage(this, "MyappInfraCdkDeployStage")
pipeline.addStage(deployStage)
```
DeployStageの中にリポジトリごとのstackを作成し、各stackでデプロイ用のCodePipelineやECSを作成しています

## デプロイ用のCodePipeline
バッチ処理のリポジトリのコード反映用(Source, Build)のCodePipelineを作成します

```Kotlin:BatchStack.kt
class BatchStack(parent: Construct, name: String): Stack(parent, name) {
    init {
        var ecrRepository: Repository = Repository.Builder
            .create(this, "developmentMyappBatchCdkRepository")
            .repositoryName("development-myapp-batch-cdk-repository")
            .encryption(RepositoryEncryption.KMS)
            .build()

        val awsDefaultRegion = this.getNode().tryGetContext("awsDefaultRegion").toString()
        val connectionArn = context["batchConnectionArn"].toString()
        val owner = context["githubOwner"].toString()
        val repository = context["batchRepository"].toString()
        val branch = context["batchBranch"].toString()

        val ecrRepositoryUri = ecrRepository.getRepositoryUri()

        val sourceArtufact: Artifact = Artifact.artifact("SourceArtifact")

        val github: Action = CodeStarConnectionsSourceAction.Builder
            .create()
            .actionName("Source")
            .owner(owner)
            .repo(repository)
            .branch(branch)
            .output(sourceArtufact)
            .connectionArn(connectionArn)
            .build()

        val source: StageOptions = StageOptions.builder()
            .stageName("Source")
            .actions(listOf(github))
            .build()

        val buildArtifact: Artifact = Artifact.artifact("BuildArtifact")

        val role: Role = Role.Builder
            .create(this, "developmentMyappBatchCdkAutoDeployRole")
            .roleName("developmentMyappBatchCdkAutoDeployRole")
            .assumedBy(ServicePrincipal("codebuild.amazonaws.com"))
            .managedPolicies(listOf(
                ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ContainerRegistryPowerUser")
            ))
            .build()

        val codeBuildProject: PipelineProject = PipelineProject.Builder
            .create(this, "developmentMyappBatchCdkProject")
            .projectName("developmentMyappBatchCdkProject")
            .environment(BuildEnvironment.builder()
                .buildImage(LinuxBuildImage.STANDARD_4_0)
                .privileged(true)
                .build()
            )
            .environmentVariables(mapOf(
                "AWS_DEFAULT_REGION" to BuildEnvironmentVariable.builder().value(awsDefaultRegion).build(),
                "ENV" to BuildEnvironmentVariable.builder().value("development").build(),
                "REPOSITORY_URI" to BuildEnvironmentVariable.builder().value(ecrRepositoryUri).build()
            ))
            .role(role)
            .build()

        val codeBuild: Action = CodeBuildAction.Builder
            .create()
            .actionName("Build")
            .project(codeBuildProject)
            .input(sourceArtufact)
            .outputs(listOf(buildArtifact))
            .build()

        val build: StageOptions = StageOptions.builder()
            .stageName("Build")
            .actions(listOf(codeBuild))
            .build()

        val pipeline: Pipeline = Pipeline.Builder
            .create(this, "developmentMyappBatchCdkAutoDeployPipeline")
            .pipelineName("developmentMyappBatchCdkAutoDeployPipeline")
            .build()

        pipeline.addStage(source)
        pipeline.addStage(build)
    }
}
```

CodeBuildで環境変数渡したいときは `environmentVariables`
CodeBuildでビルド時にdocker image構築するなら `privileged(true)`

これでビルドとECRへのプッシュ(アプリケーションのbuild.specで実行している)を行うCodePipelineが作成されます

![スクリーンショット 2023-06-12 14.31.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457018/6603af30-6fd3-579e-9273-10cc33a3926d.png)

### ECSのデプロイを追加
API用、管理画面用の２つはECSへのデプロイまで行いたいので、この2つはstageを追加します

```Kotlin
val serviceArn = context["apiEcsServiceArn"].toString()
val service = BaseService.fromServiceArnWithCluster(this, "developmentMyappApiCdkEcsService", serviceArn)

val ecsDeploy: Action = EcsDeployAction.Builder
    .create()
    .actionName("Deploy")
    .variablesNamespace("DeployVariables")
    .input(buildArtifact)
    .service(service)
    .build()

val deploy: StageOptions = StageOptions.builder()
    .stageName("Deploy")
    .actions(listOf(ecsDeploy))
    .build()

pipeline.addStage(deploy)
```

しかし、これをデプロイすると
```
Exception in thread "main" java.lang.RuntimeException: Error: Pipeline stack which uses cross-environment actions must have an explicitly set region
```
のエラーが発生します
アカウント情報などを渡す必要があるようなので、App.ktから各リポジトリごとのstackまでstackPropsを渡すように変更します
```Kotlin:App.kt
fun main(args: Array<String>): Unit {
    val app = App()
    val awsAccountId = app.getNode().tryGetContext("awsAccountId").toString()
    val awsDefaultRegion = app.getNode().tryGetContext("awsDefaultRegion").toString()

    val stackProps: StackProps = StackProps.Builder()
            .env(Environment.builder()
                .region(awsDefaultRegion)
                .account(awsAccountId)
                .build())
            .build()

    InfraPipelineStack(app, "developmentMyappInfraCdkPipelineStack", stackProps)
    app.synth()
}
```
```Kotlin:BatchStack.kt
class BatchStack(
    parent: Construct,
    name: String,
    stackProps: StackProps
): Stack(parent, name, stackProps) {
}
```


これでECSにデプロイまで行うCodePipelineが完成です
![スクリーンショット 2023-06-12 15.48.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457018/411a7084-651d-2ff3-2bc8-dbb67202cf23.png)


## 次回
EventBridge Schedulerでバッチの定義をする