search
LoginSignup
4
Help us understand the problem. What are the problem?

posted at

updated at

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

概要

画像1.png
OS:Windows11
開発環境:VisualStudio2022(Docker使用)
プロジェクト:.NET Core Razor Pages
ソース管理:GitLab(セルフホスト版)


Visual StudioでDockerを使用する方法については、こちらの記事で説明しています。


AWSの環境はTerraFormで構築しました。
TerraFormを使ったAWS環境構築については、今回の趣旨から逸れるのでここでは説明しません。

上記の環境で、GitLabにソースをプッシュしたら、ECSに自動でデプロイされるように設定していきます。
CI/CDの処理は全て「.gitlab-ci.yml」というファイルに記述します。

プロジェクトのフォルダ構成

├─.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の中身は同じです。

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

手順1. GitLab Runnerを設定する

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



下記のコマンドを、コマンドプロンプトで実行して、GitLab Runnerコンテナを起動します。
volume作成
docker volume create gitlab-runner-config
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

GitLab Runnerコンテナを起動したら、下記のコマンドを使用して、プロジェクトにランナーを登録をします。
GitLab Runnerを登録
docker run --rm -it -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest register

コマンドを実行すると、対話形式で登録が始まるので、設定していきます。

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で明示的にイメージが指定されていない場合に、使用するイメージ

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

画像2.png

GitLab Runnerの登録が完了すると、プロジェクトに登録されているランナーが表示されます。
ここで、鉛筆マークからRunnerの設定画面を開いて、「タグのないジョブの実行」にチェックを入れておきます。
画像3.png
画像4.png

手順2. 環境変数の設定

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

画像7.png

デフォルトでは、masterブランチのみ保護ブランチになっているので、Protect variableにチェックが付いているとmasterブランチ以外のプッシュでは、環境変数の値が参照出来ません。
なので、Protect variableのチェックを外すか、パイプラインを実行したいブランチを保護する必要があります。
画像8.png

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

手順3.「.gitlab-ci.yml」ファイルを作成する

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

画像6.png

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

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

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

テスト

.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コマンドを使用して、プロジェクトの単体テストを行っています。
コマンドプロンプトを開いて、プロジェクトのあるフォルダに移動して、同じコマンドを入力すれば、動きを確認出来ます。

ビルド

.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イメージの生成が出来ません。
docker build出来るようにする方法として、Docker in Dockerがありますが、特権モードでGitLab Runnerを実行する必要があったりするので、今回は「kaniko」というイメージを使ってdocker build出来るようにしました。

デプロイ

.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コマンドが使用可能になります。
services: - stedolan/jq:latest でjqコマンドも使えるようにしています。jqコマンドは、jsonデータから目的の値を抽出したり、整形したり出来るツールで、タスク定義のイメージ書き換えなどに使用します。

最初にAWS認証を行います。
その後、先ほどECRにプッシュしたイメージに、最新であることが、分かりやすいようにlatestタグを追加します。タグの追加方法はAWS公式ドキュメントを参考にしました。



次に、aws ecs describe-task-definitionでタスク定義をjsonで取得して、TASK_DEFINITIONに格納します。
タスク定義を取得する際に、del関数で「.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy」を削除しています。
この項目を削除しておかないと、後でタスク定義を読み込ませる時にエラーが出ます。

取得したタスク定義は、参照しているimage部分だけを、最新のものに書き換えて、NEW_CONTAINER_DEFINTIION に格納します。

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

画像9.png

参考文献

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4
Help us understand the problem. What are the problem?