GitLab RunnerでECR/ECSにデプロイする

# はじめに
昨日GitLabとRunnerをコンテナで手軽に構築する記事を書きましたが、今回はそれを使ってAWSのECRとECSにデプロイをしてみることにします。

https://qiita.com/handy-dd18/items/8eb4e4a754fbd8b42448

ECS上で動作するアプリケーションは以下の記事を参考にしました。

https://qiita.com/rspepe/items/c0c0119032ccc79ff7a1


# 環境
- Amazon Linux2
- Windows10(SSH用)
- WSL


# 作業手順
## リポジトリ作成
以下のURLを実行して、前回構築したGitLabにログインします。
初期パスワードがわからなければ前回の記事を見てください。
- http://<グローバルIP>/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/5a29c972-a739-9a72-7fbf-a28da1bc70d9.png)

New Projectから新しいプロジェクト(リポジトリ)を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/529105d2-a466-f108-c133-ae20182279fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/5d6854b4-7c95-4dc4-7785-1e039446786a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/7d01575a-2895-74c8-b0ac-a488711226ac.png)

プロジェクトが作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/638bab45-dfaf-c4f1-b1f9-815cbce80e2e.png)


## ECRリポジトリ作成
コンソールからECRのリポジトリとECSクラスターを作成しておきます。
ECSクラスターは"ネットワーキングのみ"で作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/7aa52a89-6384-bb20-365b-0919b6c6364f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/88d3aab4-ef4d-7da7-3eec-f1b002e8255e.png)


## ファイル作成
リポジトリページのCloneからHTTP URLをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/e52097ff-c6a9-3159-ec47-85cfc8579c45.png)

ローカルPCでWSLを起動し、作業フォルダに移動したら以下のコマンドを実行します。
※コピーしたままのURLだとhttpの後ろがランダムな文字列が含まれているので、その部分をEC2のグローバルIPに置き換えておきます。

```
$ git clone http://<グローバルIP>/root/qiita-test-pj.git
Cloning into 'qiita-test-pj'...
Username for 'http://XXX.XXX.XXX.XXX': root
Password for 'http://root@XXX.XXX.XXX.XXX':
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 210 bytes | 3.00 KiB/s, done.
```

git cloneしたフォルダに移動します。
今はリポジトリ作成時に生成されたREADME.mdファイルしかありません。

```
$ cd qiita-test-pj; ls
README.md
```

[こちら](https://qiita.com/rspepe/items/c0c0119032ccc79ff7a1)に従って、以下のファイルを作成します。
※詳細は参考サイトを見てください

- Dockerfile
- default.conf
- app/index.html


## タスク定義作成
次にECSのタスク定義を作成します。
タスク定義はコンソールで作成すると手順が多いので、jsonファイルを作成してCLIで作ります。
初回作成のみCLIで行いますが、更新はGitLab Runnerで行います。
尚、タスク定義のJSONは地味につらかったので、[この記事](https://dev.classmethod.jp/articles/ecs-task-definition-to-json/)を参考に作成しました。

```task-definition.json
{
    "executionRoleArn": "arn:aws:iam::999999999999:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/task-qiita-ecr",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "portMappings": [
                {
                    "hostPort": 80,
                    "protocol": "tcp",
                    "containerPort": 80
                }
            ],
            "cpu": 0,
            "environment": [],
            "mountPoints": [],
            "memoryReservation": 128,
            "volumesFrom": [],
            "image": "999999999999.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-test-pj-repo",
            "essential": true,
            "name": "qiita-test-container"
        }
    ],
    "memory": "512",
    "family": "task-qiita-ecr",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "cpu": "256",
    "inferenceAccelerators": [],
    "volumes": []
}
```

上記の設定の中で主に設定が必要なパラメータは以下の5つです。
後は構築したい環境によってパラメータを追加、削除してください。

|パラメータ名|設定内容|
|:--|:--|
|executionRoleArn|タスク実行ロールのARNを指定|
|containerDefinitions.logConfiguration.options.awslogs-group|ECSログの出力先ロググループ名を指定|
|containerDefinitions.image|ECSで利用するECRリポジトリを指定|
|containerDefinitions.name|ECSで実行されるコンテナの名前を設定|
|family|タスク定義の名前を指定|

以下のコマンドを実行し、タスク定義を作成します。

```
$ aws ecs register-task-definition --family task-qiita-ecr --cli-inpu
t-json file://task-definition.json
{
    "taskDefinition": {
        "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:941996685139:task-definition/task-qiita-ecr:1",
        "containerDefinitions": [
            {
                "name": "qiita-test-container",
                "image": "999999999999.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-test-pj-repo",
                "cpu": 0,
                "memoryReservation": 128,
                "portMappings": [
                    {
                        "containerPort": 80,
                        "hostPort": 80,
                        "protocol": "tcp"
                    }
                ],
                "essential": true,
                "environment": [],
                "mountPoints": [],
                "volumesFrom": [],
                "logConfiguration": {
                    "logDriver": "awslogs",
                    "options": {
                        "awslogs-group": "/ecs/task-qiita-ecr",
                        "awslogs-region": "ap-northeast-1",
                        "awslogs-stream-prefix": "ecs"
                    }
                }
            }
        ],
        "family": "task-qiita-ecr",
        "executionRoleArn": "arn:aws:iam::999999999999:role/ecsTaskExecutionRole",
        "networkMode": "awsvpc",
        "revision": 1,
        "volumes": [],
        "status": "ACTIVE",
        "requiresAttributes": [
            {
                "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
            },
            {
                "name": "ecs.capability.execution-role-awslogs"
            },
            {
                "name": "com.amazonaws.ecs.capability.ecr-auth"
            },
            {
                "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
            },
            {
                "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
            },
            {
                "name": "ecs.capability.execution-role-ecr-pull"
            },
            {
                "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
            },
            {
                "name": "ecs.capability.task-eni"
            }
        ],
        "placementConstraints": [],
        "compatibilities": [
            "EC2",
            "FARGATE"
        ],
        "requiresCompatibilities": [
            "FARGATE"
        ],
        "cpu": "256",
        "memory": "512",
        "registeredAt": "2021-12-14T13:02:50.681000+00:00",
        "registeredBy": "arn:aws:iam::999999999999:user/user"
    }
}
```

コンソールから確認するとタスク定義が作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/079d4b32-3baf-9765-4e72-1f623abca64c.png)


## ECSサービス作成
今回の設定ではECSサービスの更新しかしないため、更新対象のECSサービスをあらかじめ作成しておく必要があります。

対象のクラスターのサービスタブから作成を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/2e7614cb-a843-36a6-ff34-64a1953069fd.png)

以下の内容で設定します。
タスクの数は敢えて0で設定しており、この後のRunnerで変更するように設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/deb5fb31-925e-828e-dcaf-c4753b54f695.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/5ff57a7f-09ee-bb38-7c26-3f8518abf75c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/41f0aee6-9616-819f-8607-231333661c7d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/fce7f70f-4b09-60f6-acf9-2b2c1a72724e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/aeb273c3-7cf6-0880-744f-cec6c03c7a14.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/fd662d97-1f40-c4ac-f1c0-b638a2d12095.png)


設定したサービスが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/f700ae44-1939-119f-c741-841211c80c3f.png)


## .gitlab-ci.ymlファイル作成
GitLab Runnerを実行するには、.gitlab-ci.ymlファイルが必要です。
.gitlab-ci.ymlファイルにはECRへのイメージのPushを行うBuildステージとECSのタスク定義の更新、ECSサービスの更新を行うデプロイステージを記述します。

今回は手順削減のためにIAMユーザのキーを利用しますが、本来は環境変数に入れたり、EC2のIAMロールを利用することを推奨します。

```.gitlab-ci.yml
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""
  AWS_ACCOUNT_ID: 999999999999
  AWS_ACCESS_KEY_ID: ABCDEFGHIJKLMNOPQRSTU
  AWS_SECRET_ACCESS_KEY: ABCDEFGHIJKLMNOPQRSTUABCDEFGHIJKLMNOPQRSTU
  AWS_DEFAULT_REGION: ap-northeast-1
stages:
  - build
  - deploy
build-image:
  stage: build
  tags:
    - test-runner
  image: docker:18.09.7
  services:
    - docker:18.09.7-dind
  script:
    - apk add --update py-pip
    - pip install awscli
    - aws sts get-caller-identity
    - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
    - docker build -t alpine ./repository
    - docker tag alpine:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-test-pj-repo:latest
    - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-test-pj-repo:latest
deploy-task-definition:
  stage: deploy
  tags:
    - test-runner
  image: docker:18.09.7
  services:
    - docker:18.09.7-dind
  script:
    - apk add --update py-pip
    - pip install awscli
    - aws ecs register-task-definition --cli-input-json file://task-definition.json
    - aws ecs update-service --cluster qiita-test-pj-cluster --service qiita-test-service --task-definition task-qiita-ecr --desired-count 2
```

一つだけ補足すると、最後の2つのコマンドはタスク定義とサービスの更新を行っており、update-serviceを実行することでタスク定義を最新のものに更新しています。
サービスで実行されるコンテナ数もここで更新をしています。

```
    - aws ecs register-task-definition --cli-input-json file://task-definition.json
    - aws ecs update-service --cluster nginx-ecs --service service-nginx-ecs --task-definition task-nginx-ecr --desired-count 2
```

タスク定義には以下のようなリビジョン番号があり、update-serviceをしないと更新されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/ff49bbbd-f21b-bacd-1902-d1f6d7a737e3.png)


.gitlab-ci.ymlファイル内の「tags」には登録したRunnerのタグを設定します。
Runnerのタグは以下から確認が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/844dcef1-ee43-d1cf-8dfe-f860680cace2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/41ab8e70-88ec-8b79-9e2a-125471586c86.png)


## GitLabリポジトリへPush
ここまでできたら以下のコマンドを実行し、GitLab上のリポジトリにPushをします。

```
$ git add .
$ git commit -m "commit"
$ git push origin main
```

リポジトリにファイルがアップロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/51a102e1-3dea-1c95-ebef-640aa1bdffa2.png)


## Pipeline実行確認
Pushが成功するとすぐにPipelineが実行されます。

以下の手順で実行されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/866ba535-035b-7848-9f68-bec84602d0b1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/58130648-6856-a56a-a793-ee1b67f3148b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/195bba1b-b9b0-293f-7fc4-7930e8761b87.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/d06cbd62-7fb1-0694-d7b3-eff0f557d1ee.png)

BuildとDeployが正常終了していることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/2beeed5c-ab42-0d5b-ee43-621218f1f47c.png)


## デプロイ結果確認
コンソールからECSクラスターを確認したところ、何故かコンテナが起動していませんでした。
出力されたメッセージを見ると、どうやらロググループが存在しないためエラーとなっていたようです。
(自動で作成されると記憶していましたが作成されていませんでした)

今回は手動で「/ecs/task-qiita-ecr」ロググループを作成しました。

```
 ResourceInitializationError: failed to validate logger args: create stream has been retried 1 times: failed to create Cloudwatch log stream: ResourceNotFoundException: The specified log group does not exist. : exit status 1
```

ロググループを作成してしばらくするとコンテナが起動してきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/7350fdc2-bb6d-41c7-6c74-ce1da48e11c7.png)

コンテナに設定されたパブリックIPをブラウザで確認するとデプロイしたアプリケーションが表示されたので、これでECSのデプロイまで完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/99a58fca-4d4c-545b-3226-6c3d680502d2.png)


※もし権限関係でコンテナの実行に失敗する場合は、タスク実行ロールにこのあたりのポリシーを付与するとうまくいくかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/daaa1a9a-a528-1b8d-0b3f-97ca50bcc443.png)


# おわりに
一度設定した仕組みなのでサッと作れるかと思いましたが、設定漏れが見つかったりして思ったよりも時間がかかりました。
GitLabはほとんど使ったことないのでイケてない記述があったかもしれませんが、とりあえずCICDまで動かせたのでOKでしょう。

長くなってしまいましたが、この記事がどなたかの参考になれば幸いです。