GitHub
AWS
継続的デリバリー
CircleCI
ECS
OriginalゆめみDay 12

CircleCI で GitHubのブランチ更新をフックしてAWS ECSへデプロイする

この文書について

対象読者: サーバサイドエンジニア

AWS の ECSへのデプロイ を GitHub と CircleCI を使って実施する方法を紹介します

具体的には

  • インフラ構成 (GitHub,CircleCI,AWS にそれぞれどんなデータがあるのか)
  • デプロイはどのような流れで行われるのか
  • デプロイフローの構築
    • AWS側で準備すること
    • ビルド用にソースコードリポジトリに準備しておくこと
    • CircleCI側で準備すること
    • CircleCIのYAMLファイルに書くシェルスクリプト

を説明します。

利用するツール

  • GitHub
    • いわずと知れたGitリポジトリのホスティング
    • 外部サービス連携がやりやすい
  • CircleCI 2.0
    • GitHub上のリポジトリのイベントと連携してタスクが実行できます
      • PullRequestに連動してテストコードを実行
      • ブランチのPushに連動してデプロイ (今回はコレ)
  • AWS ECS
    • Dockerイメージを管理・コンテナを起動などができるサービス

前提

  • アプリケーションは The Twelve-Factor App に準拠
    • 詳細は割愛しますが、重要なのは以下の2つ
      • I. Codebase : One codebase tracked in revision control, many deploys
      • III. Config : Store config in the environment
  • デプロイするためのブランチ名は環境別に用意する
    • deploy/development => 開発環境
    • deploy/production => 本番環境

インフラ構成とデプロイフロー

deploy_ecs_with_circleci.png

  1. エンジニアが GitHubに deployブランチをプッシュする
  2. CircleCIがブランチのPushをトリガーに起動
  3. CircleCIがGitHubの該当コミットをチェックアウト
  4. CircleCIがソースコードからDockerImageをビルドし、タグを発行
  5. CircleCIがDockerImageを AWS ECRにプッシュする
  6. CircleCIがソースコードから環境変数を抽出する
  7. CircleCIが環境変数とタグを元にECSのタスク定義を追加
  8. CircleCIがECSのサービスのタスク定義更新し、新しく作ったタスク定義を参照させる
  9. ECSが新しいコンテナを起動する

デプロイフローの構築

AWS

  • ECRのRegistry(イメージ置き場)を作成
    • 名前はソースコードリポジトリと同様にしておく
  • ECSのクラスタを作成
    • クラスタ名は {環境名}-{ソースコードリポジトリ名}-cluster
  • ECSのサービスを作成
    • サービス名は {環境名}-{ソースコードリポジトリ名}-service
  • ECSのタスク作成
    • タスク名は {環境名}-{ソースコードリポジトリ名}-task
  • ECSのタスクを変更可能なIAMユーザを作成
    • ユーザ名は {環境名}-{ソースコードリポジトリ名}-iam-role-for-ecs-task

ソースコード

  • 環境依存の変数を暗号化してコミット
    • AWSのManagementConsoleで タスク定義 を表示し、 JSONを表示
    • 表示したJSONをコピーしてgit管理しているディレクトリに保存
    • 保存したJSONの environment プロパティを編集
      • DBのエンドポイント情報など
    • 編集したJSONを openssl enc で暗号化する
      • ファイル名は {環境名}.json.crypted

CircleCI

  • GitHubのソースコードリポジトリとCircleCIの連携をする
  • CircleCIの管理画面で、対象リポジトリのビルド設定をする
    • AWSのAccessKeyとSecretAccessKeyを登録
      • ECSのタスクを変更可能なIAMユーザのもの
  • CircileCIの環境変数に暗号化したファイルの復号化パスワードを登録しておく
    • 変数名は ENCRYPTION_PASSWORD

CircleCIのYAMLファイルに書くシェルスクリプト

.circleci/config.yml の run で実行するシェルスクリプトのうち、
ECS関連部分を抜粋すると以下のとおりです。

# デプロイブランチ名から環境名を抽出
TARGET_ENV=`echo ${CIRCLE_BRANCH} | sed -e 's#deploy/##g'`
DOCKER_IMAGE_NAME={ソースコードリポジトリ名}
COMMIT_ID=$(git rev-parse HEAD)

# ECRへログイン
ECR_LOGIN_COMMAND=$(aws ecr get-login --no-include-email --region ap-northeast-1)
eval $ECR_LOGIN_COMMAND
# タグ付け
DOCKER_REGISTORY={AWSのID}.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag \
  $DOCKER_IMAGE_NAME:$COMMIT_ID \
  $DOCKER_REGISTORY/$DOCKER_IMAGE_NAME:$COMMIT_ID
# ECRへプッシュ
docker push "$DOCKER_REGISTORY/$DOCKER_IMAGE_NAME:$COMMIT_ID"

# 暗号化した環境変数の復号化
openssl enc -d -aes-256-cbc -in ${TARGET_ENV}.json.crypted -out ${TARGET_ENV}.json -k "${ENCRYPTION_PASSWORD}"
# タスク定義のjson作成 (復号化したjsonにECRに登録したイメージ:タグを注入)
FAMILY="${TARGET_ENV}-{ソースコードリポジトリ名}-task"
TEMPLATE=`cat ${TARGET_ENV}.json`
CONTAINER_DEFINITIONS=$(printf "${TEMPLATE}" ${DOCKER_IMAGE_NAME_AND_TAG})
TASK_ROLE_ARN="arn:aws:iam::{AWSのID}:role/${TARGET_ENV}-{ソースコードリポジトリ名}-iam-role-for-ecs-task"
AWS_REGION={お使いのAWSのリージョン e.g. ap-northeast-1}

# ECSのタスク定義を作成
TASK_REVISION=$(aws ecs register-task-definition \
--region "$AWS_REGION" \
--task-role-arn "$TASK_ROLE_ARN" \
--container-definitions "${CONTAINER_DEFINITIONS}" \
--family ${FAMILY} \
| jq --raw-output --exit-status '.taskDefinition.taskDefinitionArn')

SERVICE="${APP_ENV}-{ソースコードリポジトリ名}-service"
CLUSTER="${APP_ENV}-{ソースコードリポジトリ名}-cluster"

# ECSのサービスを更新
UPDATED_REVISION=$(aws ecs update-service \
--region ap-northeast-1 \
--cluster ${CLUSTER} \
--service ${SERVICE} \
--task-definition ${TASK_REVISION} \
| jq --raw-output --exit-status '.service.taskDefinition')

課題と改善のアイデア

この方法ではタスク定義にDB接続情報などが埋め込まれます。

このため、AWSのManagementConsoleのECSの権限さえもっていれば、
こうした情報を運用担当者が確認できてしまいます。

改善案としては、タスク定義に埋め込むのは暗号化した値とし、
アプリケーション内で復号化をしたほうがより安全になります。

候補 : AWS KMS