手動デプロイにさようなら!GitLab CIで実現するECS/ECRへの自動デプロイ

AWS

はじめに

「ECSへのデプロイは、毎回手順書見ながら進めている」
「デプロイミスで本番環境が落ちてしまった」
「いつも同じ作業の繰り返しで、本来やるべき開発に時間が取れない…」

こんな悩みを抱えているエンジニアの方も多いのではないでしょうか

実際、コンテナデプロイの現場では以下のような課題に直面することが少なくありません

  • 手動デプロイによるヒューマンエラー
  • 複雑なデプロイ手順の属人化
  • デプロイ作業による開発時間の圧迫
  • 急なリリース要望への対応の遅れ

しかし、これらの課題は自動化で解決できます

本記事では、GitLab CI/CDを使ってAWS ECS/ECRへの自動デプロイを実現する方法を解説します
手順書に頼らない、効率的なパイプラインの構築方法をステップバイステップで説明していきます

  • 手動デプロイの工数削減を検討している方
  • ECR+ESCへのCI/CDパイプラインの構築をしようと考えている方
  • コンテナデプロイの自動化に興味のある方
  • ECS、ECSの基本的な概念
  • GitLab CI/CDの基本的な使用方法
  • Dockerの基礎知識

パイプライン

まずは、今回構築するCI/CDパイプラインの全体像はこちらです

パイプラインは、大きく分けて2つのステージで構成しています
フローを順番に見ていきましょう

ビルドステージ

開発者がGitLabにバージョンタグ(例:v1.0.0)をプッシュすると、まずビルドステージが実行されます

  1. Dockerイメージのビルド
  2. Amazon ECRへのログイン
  3. ビルドしたイメージをECRへプッシュ

デプロイステージ

ビルドステージが成功すると、続いてデプロイステージが実行されます

  1. 現在のECSタスク定義の取得
  2. 新しいイメージ情報でタスク定義を更新
  3. ECSサービスの更新
  4. デプロイ完了まで待機

AWSリソースの事前準備

実際に構成を試す方は、以下のリソースを用意しておきましょう

✅ ネットワーク基盤

  • VPC・サブネットなどの作成

✅ Amazon ECR

  • リポジトリの作成

✅ Amazon ECS

  • ECSクラスターの作成
  • ECSサービスの作成
  • タスク定義の作成

以下のリソース名はパイプラインの環境変数で利用するのでメモしておきましょう

ECRレジストリ: xxx.dkr.ecr.ap-northeast-1.amazonaws.com
ECRリポジトリ: your-repo-name
ECSクラスター: your-cluster-name
ECSサービス: your-service-name
タスク定義: your-task-definition

.gitlab-ci.ymlの解説

AWSリソースが用意できたところで、本題の.gitlab-ci.ymlです
(ECS、ECRに関する部分のみの解説になります)

なおAWSの認証情報の取得には、GitLabのOpenID Connect(OIDC)を利用することを推奨します

これにより、AWSの一時的なクレデンシャルを安全に取得でき、アクセスキーの管理が不要になります

詳細な設定方法はConfigure OpenID Connect in AWS to retrieve temporary credentials | GitLab Docsを参照してください

services:
  - docker:dind

variables:
  AWS_DEFAULT_REGION: ap-northeast-1

stages:
  - build
  - deploy

image-build:
  stage: build
  image: docker:latest
  script:
    - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
    - docker build -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG} .
    - docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG}
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/   

task-deploy:
  stage: deploy
  image: docker:latest
  script:
    - export NEW_IMAGE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG}"
    - aws ecs describe-task-definition --task-definition ${TASK_DEFINITION} --output json > temp.json
    - jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' temp.json > task-def.json
    - jq --arg NEW_IMAGE "${NEW_IMAGE}" '.containerDefinitions[0].image = $NEW_IMAGE' task-def.json > new-task-def.json
    - aws ecs register-task-definition --cli-input-json file://new-task-def.json
    - aws ecs update-service --cluster ${ECS_CLUSTER} --service ${ECS_SERVICE} --task-definition ${TASK_DEFINITION}
    - aws ecs wait services-stable --cluster ${ECS_CLUSTER} --services ${ECS_SERVICE}
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/  

順番に各要素を解説していきます

環境変数

まずGitLabの設定から環境変数を登録しておきます

# ECRへのプッシュに必要な変数
ECR_REGISTRY: ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com
ECR_REPOSITORY: your-repository-name

# ECSの更新に必要な変数
ECS_CLUSTER: your-cluster-name
ECS_SERVICE: your-service-name
TASK_DEFINITION: your-task-definition-name

ビルドステージ

ECRへの認証

まず、get-login-passwordでECRへの一時的なパスワードを取得し、
そのパスワードを使ってECRへの認証を行っています

# ECRへのログイン
aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}

イメージのビルドとプッシュ

続いてDockerイメージのビルドを行います
tag名はGitのタグを参照しバージョン情報を付与しています
例えば、xxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/your-repo-name:v1.0.0 のような形です

そして、docker pushでビルドしたイメージをECRへpushします

# イメージのビルドとプッシュ
docker build -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG} .
docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG}

デプロイステージ

既存のタスク定義を取得

デプロイステージでは、describe-task-definitionを使って、
既存のタスク定義(JSON)を取得します

既存のJSONにはtaskDefinitionArnやrevisionなどの、この後の登録に不要となるフィールドが含まれているので、不要なフィールドはjqコマンドを使って削除しています

# 現在のタスク定義を取得
aws ecs describe-task-definition --task-definition ${TASK_DEFINITION} --output json > temp.json

# 不要なフィールドを削除
jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' temp.json > task-def.json

タスク定義の中身を更新

続いて、containerDefinitions[0].Imageフィールドの値を新しいイメージに書き換えることで、
タスク定義の更新を行います

これで新しいイメージでタスクを実行するように定義を更新できました

# 新しいイメージURIを設定
jq --arg NEW_IMAGE "${NEW_IMAGE}" '.containerDefinitions[0].image = $NEW_IMAGE' task-def.json > new-task-def.json

タスク定義を登録

register-task-definitionコマンドで更新したタスク定義JSONを使用して、
新しいリビジョンの登録を行います

# 新しいタスク定義を登録
aws ecs register-task-definition --cli-input-json file://new-task-def.json

サービスを更新

新しいタスク定義でサービスを更新します

wait services-stableコマンドで新しいタスクが正常に起動するまで待機します

少し待つと、ECSのサービスが更新され新しいタスクが起動します

# サービスを更新
aws ecs update-service --cluster ${ECS_CLUSTER} --service ${ECS_SERVICE} --task-definition ${TASK_DEFINITION}

# サービスが安定するまで待機
aws ecs wait services-stable --cluster ${ECS_CLUSTER} --services ${ECS_SERVICE}ECS

イメージタグ戦略のベストプラクティス

最後に、ECS/ECRでのイメージタグ戦略について解説します

セマンティックタグで実行

今回のパイプラインはGitにv1.0.0のようなセマンティックなタグ(推奨される方法)が
付けられている場合のみ実行されるようにしています

rules:
  - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/   

script:
  - docker build -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_TAG} .

では、具体的に推奨されるタグについて見ていきましょう

推奨されるタグ付け

タグはv1.0.0のように「メジャーバージョン.マイナーバージョン.パッチバージョン」で表現したものをつけます。また、一度付けたタグは決して上書きしてはいけません

# 以下のような形
your-repo:v1.0.1
your-repo:v1.1.0
your-repo:v2.0.0

なぜセマンティックバージョニングが重要か?

①バージョンの意味が明確

セマンティックバージョニングでは、バージョン番号の変更から、
その変更の影響度を即座に判断することができます

例えば:

  • v1.0.0 → v2.0.0への更新は、APIの変更や互換性のない修正など、破壊的な変更を含みます
  • v1.0.0 → v1.1.0は新機能の追加を表し、既存の機能との互換性は維持されています
  • v1.0.0 → v1.0.1はバグ修正のみの更新で、機能的な変更は含まれません

このように、バージョン番号を見るだけで変更の重要度が分かるため、
デプロイ時のリスク評価やレビューの重点化が容易になります

②運用上のメリット

運用面でのメリットもあります

まず、デプロイ履歴が追跡しやすくなります
いつ、どのような変更がデプロイされたのか、バージョン番号から即座に把握できます

また、本番環境で問題が発生した際も、どのバージョンから問題が発生したのか特定が容易です
例えば、v1.1.0へのアップデート後に問題が見つかれば、新機能の追加が原因である可能性が高いと推測できます

さらに、問題が発生した際のロールバックも確実に行えます
「直前の安定バージョン」が明確なため、迅速な対応が可能です

使用を避けるべきタグ

一方で、開発現場ではよく見かけるものの、本番環境では避けるべきタグの付け方もあります

以下のようなタグ付けは、運用上の問題を引き起こす可能性があるため注意が必要です

# アンチパターン
your-repo:latest    # どのバージョンが使われているか不明確
your-repo:stable    # 「安定版」の定義があいまい
your-repo:test      # 一時的なタグは避ける

具体的に見ていきましょう

①予期せぬ更新の可能性

例えばlatestタグは「最新のイメージ」を指すため、他のチームメンバーのデプロイによって、
知らないうちに参照先のバージョンが変わってしまう可能性があります

これは予期せぬ障害の原因となりかねません

# 本番環境で動いているバージョン
your-repo:latest  # 現在はv1.0.0を指している

# 新バージョンのデプロイ
your-repo:latest  # いつの間にかv1.1.0を指すように変更される

②ロールバックの困難さ

問題が発生した際、latestタグではどのバージョンに戻せばよいのか判断が難しくなります

このような理由から、本番環境では必ずセマンティックバージョニングを使用し、
各デプロイで明確なバージョン管理を行うことが推奨されています

またタグ戦略は、チーム全体で合意し、一貫性を持って運用することが重要です

まとめ

本記事では、GitLab CIを使ってAWS ECS/ECRへの自動デプロイを実現する方法を解説してきました

実装のポイント

  • 2ステージ構成(build, deploy)でシンプルに実現
  • セマンティックバージョニングによるデプロイ制御
  • ECSサービスの更新から完了確認までを自動化

導入効果

  • デプロイ作業の工数削減
  • ヒューマンエラーの防止
  • デプロイ手順の標準化
  • 迅速なリリース対応が可能に

この記事が、みなさんのデプロイ自動化への第一歩となれば幸いです

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!