.NET CoreのプロジェクトをGitLab CI/CDでECSに自動デプロイさせる



<h1>概要</h1>

![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/d387be99-e329-8ead-a04e-205f747ec3d4.png)
OS：Windows11<br>開発環境：VisualStudio2022（Docker使用）<br>プロジェクト：.NET Core Razor Pages<br>ソース管理：GitLab（セルフホスト版）
<br>
Visual StudioでDockerを使用する方法については、こちらの記事で説明しています。

https://qiita.com/waka0238/items/d28405f2e3a635a7c04c

<br>AWSの環境はTerraFormで構築しました。<br>TerraFormを使ったAWS環境構築については、今回の趣旨から逸れるのでここでは説明しません。
<br>上記の環境で、GitLabにソースをプッシュしたら、ECSに自動でデプロイされるように設定していきます。<br>CI/CDの処理は全て「.gitlab-ci.yml」というファイルに記述します。

<h1>プロジェクトのフォルダ構成</h1>

```text:
├─.git
├─.vs
├─bin
├─db
├─obj
│
├─RazorPageSample
│  │
│  ├─bin
│  ├─Data
│  ├─Migrations
│  ├─Models
│  ├─obj
│  ├─Pages
│  ├─Properties
│  ├─wwwroot
│  ├─appsettings.Development.json
│  ├─appsettings.json
│  ├─Dockerfile
│  ├─Program.cs
│  ├─RazorPageSample.csproj
│  └─RazorPageSample.csproj.user
│
├─RazorPageSampleTests
│  │
│  ├─bin
│  ├─obj
│  ├─CreateModelTests.cs
│  ├─DeleteModelTests.cs
│  ├─EditModelTests.cs
│  └─obj
│
├─TestResults
│
│  .dockerignore
│  .gitignore
│  .gitlab-ci.yml
│  docker-compose.dcproj
│  docker-compose.override.yml
│  docker-compose.yml
│  Dockerfile
│  RazorPageSample.sln
```

Dockerfileが「./Dockerfile」と「./RazorPageSample/Dockerfile」の二つ存在していますが、「./RazorPageSample/Dockerfile」を.gitlab-ci.ymlで指定して、docker buildすると、Program.csにMainメソッドが省略された関係で、ビルドエラーになってしまったので、.gitlab-ci.ymlと同じ階層にもDockerfileを配置しています。Dockerfileの中身は同じです。<br>

./Dockerfile・・・.gitlab-ci.ymlでdocker buildする時に指定するDockerfile
./RazorPageSample/Dockerfile・・・VisualStudioでdocker buildする時に参照されるDockerfile



<h1>手順１. GitLab Runnerを設定する</h1>
GitLab CI/CDを動かすためには、GitLab Runnerが必要です。<br>GitLab Runnerを動かすためには、Dockerが必要なので、入っていない方はインストールしてください。
<br>
今回は、自分のPCにGitLab Runnerを、公式ドキュメントに沿ってインストールしていきます。

https://gitlab-docs.creationline.com/runner/install/docker.html

<br>
<br>
下記のコマンドを、コマンドプロンプトで実行して、GitLab Runnerコンテナを起動します。

```powershell:volume作成
docker volume create gitlab-runner-config
```
```powershell:GitLab Runnerコンテナを起動
docker run -d --name gitlab-runner --restart always -v /var/run/docker.sock:/var/run/docker.sock -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest
```
<br>
GitLab Runnerコンテナを起動したら、下記のコマンドを使用して、プロジェクトにランナーを登録をします。<br>

```powershell:GitLab Runnerを登録
docker run --rm -it -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest register
```
<br>
コマンドを実行すると、対話形式で登録が始まるので、設定していきます。

https://gitlab-docs.creationline.com/runner/register/index.html#docker

```:
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
GitLabインスタンスのURLを入力
Please enter the gitlab-ci token for this runner
トークンを入力
Please enter the gitlab-ci description for this runner
ランナーの説明を入力（後で変更可能）
Please enter the gitlab-ci tags for this runner (comma separated):
ランナーにタグを付ける（後で変更可能）
Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker
Please enter the Docker image (eg. ruby:2.6):
.gitlab-ci.ymlで明示的にイメージが指定されていない場合に、使用するイメージ
```

<br>
GitLabインスタンスのURLとトークンは、プロジェクトのページの[設定]>[CI/CD]を開いて、Runnerの「specific のRunnerのマニュアルセットアップ」にあります。

![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/b4db6a5a-559d-5753-23a4-325f48a2b0ea.png)

GitLab Runnerの登録が完了すると、プロジェクトに登録されているランナーが表示されます。
ここで、鉛筆マークからRunnerの設定画面を開いて、「タグのないジョブの実行」にチェックを入れておきます。
![画像3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/bb0ad70b-918e-1c21-9d53-6c639e4156fc.png)
![画像4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/2ced840a-e17d-63ad-6c97-302f97c50cf0.png)

<h1>手順２. 環境変数の設定</h1>
デプロイに必要な情報です。<br>
CI/CD処理にAWSのアカウント情報などを、直接書き込むのはよくないので、環境変数に設定して、参照できるようにします。<br>

![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/62b0394d-c2aa-fbc1-1236-cc4b887417e3.png)

デフォルトでは、masterブランチのみ保護ブランチになっているので、Protect variableにチェックが付いているとmasterブランチ以外のプッシュでは、環境変数の値が参照出来ません。<br>なので、Protect variableのチェックを外すか、パイプラインを実行したいブランチを保護する必要があります。
![画像8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/abd221c9-d555-964f-82bb-81f0238c0159.png)

自分で設定した環境変数以外にも、デフォルトで参照可能な環境変数もあります。

https://gitlab-docs.creationline.com/ee/ci/variables/predefined_variables.html

<h1>手順３.「.gitlab-ci.yml」ファイルを作成する</h1>
プロジェクトページのリポジトリから、新規ファイルを開いて「.gitlab-ci.yml」を選択して、プロジェクトにファイルを追加します。

![画像6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/b5a9a184-a066-f01d-6c5d-07c4c704c621.png)

追加した.gitlab-ci.ymlをVScodeなどで開いて、処理を書いていきます。<br>テスト、ビルド、ECSにデプロイする場合の処理は、下記のような処理になります。

```yml:.gitlab-ci.yml
stages:
  - test
  - build
  - deploy

tests:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:6.0
  script:
    # 依存関係を復元して、ビルドして、テストを実行
    - cd ./RazorPageSample
    - dotnet restore --packages .nuget
    - dotnet build --no-restore
    - dotnet test ../RazorPageSampleTests

build_image_with_kaniko:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    # Dockerイメージを生成して、イメージのタグにコミットIDを付けてECRにプッシュ
    - mkdir -p /kaniko/.docker
    - echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
    - >-
      /kaniko/executor
      --context ${CI_PROJECT_DIR}
      --dockerfile ${CI_PROJECT_DIR}/Dockerfile
      --destination ${AWS_ECR_REPOSITORY_URL}:${CI_COMMIT_SHORT_SHA}
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

deploy_image:
  stage: deploy
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
  services:
    - stedolan/jq:latest
  script:
    # AWS認証情報表示とログイン
    - aws sts get-caller-identity
    - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com

    # ECRにプッシュしたイメージにlatestタグを追加
    - MANIFEST=$(aws ecr batch-get-image 
      --repository-name ${AWS_ECR_REPOSITORY_NAME} 
      --region ${AWS_DEFAULT_REGION} 
      --image-ids imageTag=${CI_COMMIT_SHORT_SHA} 
      --output json | jq --raw-output --join-output '.images[0].imageManifest')
    - aws ecr put-image 
      --repository-name ${AWS_ECR_REPOSITORY_NAME} 
      --region ${AWS_DEFAULT_REGION} 
      --image-tag latest 
      --image-manifest "${MANIFEST}"

    # タスク定義を取得して参照しているイメージを、最新のイメージに書き換える
    - TASK_DEFINITION=$(aws ecs describe-task-definition 
      --task-definition ${AWS_ECS_TASK_DEFINITION} 
      --region ${AWS_DEFAULT_REGION} | jq '.taskDefinition | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')
    - NEW_CONTAINER_DEFINTIION=$(echo ${TASK_DEFINITION} | jq --arg IMAGE "${AWS_ECR_REPOSITORY_URL}:${CI_COMMIT_SHORT_SHA}" '.containerDefinitions[0].image = $IMAGE | .')
    
    # 書き換えたタスク定義を読み込ませて、ECSを更新する
    - aws ecs register-task-definition --cli-input-json "${NEW_CONTAINER_DEFINTIION}"
    - aws ecs update-service 
      --region ${AWS_DEFAULT_REGION} 
      --cluster ${AWS_ECS_CLUSTER} 
      --service ${AWS_ECS_SERVICE} 
      --task-definition ${AWS_ECS_TASK_DEFINITION} 
      --desired-count 1
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
```

test、build、deployの3段階に処理が分かれています。<br>複雑そうに見えますが、AWS CLIなどのコマンドを1つずつ実行しているだけなので、慣れたらそこまで難しくはないと思います。<br>buildとdeployはrulesを使って、developブランチがプッシュされたときのみ、ジョブを実行するように設定しています。rules以外にもonlyなどの機能がありますが、現在はrulesの使用が推奨されています。

では、testステージの処理から1つずつ見ていきます。

<h3>テスト</h3>

```yml:.gitlab-ci.yml
tests:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:6.0
  script:
    # 依存関係を復元して、ビルドして、テストを実行
    - cd ./RazorPageSample
    - dotnet restore --packages .nuget
    - dotnet build --no-restore
    - dotnet test ../RazorPageSampleTests
```
ここの処理では、単純にdotnetコマンドを使用して、プロジェクトの単体テストを行っています。<br>コマンドプロンプトを開いて、プロジェクトのあるフォルダに移動して、同じコマンドを入力すれば、動きを確認出来ます。

<h3>ビルド</h3>

```yml:.gitlab-ci.yml
build_image_with_kaniko:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    # Dockerイメージを生成して、イメージのタグにコミットIDを付けてECRにプッシュ
    - mkdir -p /kaniko/.docker
    - echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
    - >-
      /kaniko/executor
      --context ${CI_PROJECT_DIR}
      --dockerfile ${CI_PROJECT_DIR}/Dockerfile
      --destination ${AWS_ECR_REPOSITORY_URL}:${CI_COMMIT_SHORT_SHA}
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
```
GitLab RunnerはDockerコンテナで動いているため、そのままだとdocker buildが実行出来ず、Dockerイメージの生成が出来ません。<br>docker build出来るようにする方法として、Docker in Dockerがありますが、特権モードでGitLab Runnerを実行する必要があったりするので、今回は「kaniko」というイメージを使ってdocker build出来るようにしました。

https://gitlab-docs.creationline.com/ee/ci/docker/using_kaniko.html

<h3>デプロイ</h3>

```yml:.gitlab-ci.yml
deploy_image:
  stage: deploy
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
  services:
    - stedolan/jq:latest
  script:
    # AWS認証情報表示とログイン
    - aws sts get-caller-identity
    - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com

    # ECRにプッシュしたイメージにlatestタグを追加
    - MANIFEST=$(aws ecr batch-get-image 
      --repository-name ${AWS_ECR_REPOSITORY_NAME} 
      --region ${AWS_DEFAULT_REGION} 
      --image-ids imageTag=${CI_COMMIT_SHORT_SHA} 
      --output json | jq --raw-output --join-output '.images[0].imageManifest')
    - aws ecr put-image 
      --repository-name ${AWS_ECR_REPOSITORY_NAME} 
      --region ${AWS_DEFAULT_REGION} 
      --image-tag latest 
      --image-manifest "${MANIFEST}"

    # タスク定義を取得して参照しているイメージを、最新のイメージに書き換える
    - TASK_DEFINITION=$(aws ecs describe-task-definition 
      --task-definition ${AWS_ECS_TASK_DEFINITION} 
      --region ${AWS_DEFAULT_REGION} | jq '.taskDefinition | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')
    - NEW_CONTAINER_DEFINTIION=$(echo ${TASK_DEFINITION} | jq --arg IMAGE "${AWS_ECR_REPOSITORY_URL}:${CI_COMMIT_SHORT_SHA}" '.containerDefinitions[0].image = $IMAGE | .')
    
    # 書き換えたタスク定義を読み込ませて、ECSを更新する
    - aws ecs register-task-definition --cli-input-json "${NEW_CONTAINER_DEFINTIION}"
    - aws ecs update-service 
      --region ${AWS_DEFAULT_REGION} 
      --cluster ${AWS_ECS_CLUSTER} 
      --service ${AWS_ECS_SERVICE} 
      --task-definition ${AWS_ECS_TASK_DEFINITION} 
      --desired-count 1
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
```
最後に、deployステージです。
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest でAWS CLIコマンドが使用可能になります。<br>services: - stedolan/jq:latest でjqコマンドも使えるようにしています。jqコマンドは、jsonデータから目的の値を抽出したり、整形したり出来るツールで、タスク定義のイメージ書き換えなどに使用します。<br>
最初にAWS認証を行います。<br>その後、先ほどECRにプッシュしたイメージに、最新であることが、分かりやすいようにlatestタグを追加します。タグの追加方法はAWS公式ドキュメントを参考にしました。

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-retag.html

<br>
<br>
次に、aws ecs describe-task-definitionでタスク定義をjsonで取得して、TASK_DEFINITIONに格納します。<br>タスク定義を取得する際に、del関数で「.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy」を削除しています。<br>この項目を削除しておかないと、後でタスク定義を読み込ませる時にエラーが出ます。
<br>
<br>
取得したタスク定義は、参照しているimage部分だけを、最新のものに書き換えて、NEW_CONTAINER_DEFINTIION に格納します。

<br>
<br>
そして、aws ecs register-task-definition --cli-input-json "${NEW_CONTAINER_DEFINTIION}" でタスク定義を読み込ませて、aws ecs update-service でECS諸々を更新して、デプロイ完了です。

![画像9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1893891/3bbccade-d687-778d-e252-471b3f131419.png)

<h1>参考文献</h1>

https://medium.com/swlh/deploying-your-app-with-ecs-gitlab-ci-cd-e211e6f103e1

https://dev.classmethod.jp/articles/describe-task-definition-to-register-task-definition/

https://blog.msysh.me/posts/2020/12/transform_task_definition_by_describe_to_be_able_to_register.html

https://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af