バックエンド側 node.js AWS環境デプロイ(CodePipeline/Blue-Green Deployment)

2023-11-06

パクリ23発目!

パクリ元

内容

以下一覧を完了させて ECS リリースさせることでバックエンド側を終わりとする。順序は適宜入れ替える。

  • 開発環境構築
  • プロジェクト作成・最適なフォルダ構成
  • 認証チェック(含めた前処理) 
  • ログ処理
  • APIによってはIPチェックやHeaderチェック
  • バリデーション
  • 環境ファイルによる振り分け
  • DB参照・更新
  • 外部API呼び出し
  • 外部API呼び出し待ち合わせ
  • 認証・認可
  • SPA における CSRF CORS
  • 長時間処理を非同期処理で
  • メール送信
  • 画像アップロード
  • 画像ダウンロード
  • EXCEL/PDF作成
  • テスト
  • (AWS環境デプロイ)
  • Docker 化確認
  • CodePipeline    ←←← 今回ココ
  • RDS 対応
  • CloudFront

AWS環境へデプロイ(CodePipeline)

CodePipeline で自動でできることを周回遅れ的に知ったので試す。

CodeCommit から ECR まで

まずは以前設定したように CodeCommit で「NodeCommitTest」というリポジトリ作成。

ローカルにリポジトリをCloneする。

~$ git clone ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/NodeCommitTest
Cloning into 'NodeCommitTest'...
warning: You appear to have cloned an empty repository.

前回 Docker 化させたファイル郡と buildspec.yml をこの「NodeCommitTest」にぶち込む。buildspec.yml は以前のものに –target オプションだけつけたもの。

AWS接続環境:~/CodeCommitTest/buildspec.yml
version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - 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
      # Docker Hub へのログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKER_HUB_PASS | docker login -u $DOCKER_HUB_USER --password-stdin
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...          
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG --target runner .
      - echo docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - printf '[{"name":"<container-definition>","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > artifacts.json

artifacts:
  files: artifacts.json
NodeCommitTest
┣━ .git
┣━ __tests__
┣━ prisma
┣━ src
┣━ buildspec.yml
┣━ Dockerfile
┣━ package-lock.json
┣━ package.json
┗━ tsconfig.json

Commit、Pushする。

~/NodeCommitTest$ git add *
~/NodeCommitTest$ git commit -m "first commit"
[master (root-commit) 389101b] first commit
 33 files changed, 11643 insertions(+)
・・・
~/NodeCommitTest$ git push
・・・
 * [new branch]      master -> master

次にこれも以前やったようにまず「node-test」という ECR リポジトリ作成。

そして「NodeBuildTest」という CodeBuild プロジェクトを作成する。

「codebuild-NodeBuildTest-service-role」ロールに「AmazonEC2ContainerRegistryPowerUser」ポリシーをアタッチする。

CodePipeline 作成

まずはここまでをパクリ元1つ目を参考にパイプラインを作成する。

「NodePipelineTest」というパイプライン名にする。

ソースステージのソースプロバイダーには「AWS CodeCommit」、リポジトリ名には「NodeCommitTest」、ブランチ名は「master」を指定する。

ビルドステージのプロジェクト名には「NodeBuildTest」を指定する。

デプロイステージは一旦スキップする。

作成前の確認画面。

完了すると、「失敗しました。アクセス権限がありません」と画面に表示されていたが少し待っていたら実行されている模様。

ソース取得もビルドもうまくいった模様。

ECR への登録完了。

ECS

これまた以前設定したように ECS で「NodeClusterTest」というクラスター作成。

次に「NodeTaskTest」というタスク定義を作成する。

タスク実行ロールは ECR読み取りポリシー追加済みの「ecsTaskExecutionRole」、コンテナ名を「NodeContainerTest」、イメージURI は「<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/node-test:latest」を指定。

次に「NodeTaskTest」タスク定義を指定して「NodeTaskServiceTest」サービスを作成する。

作成されたサービスのタスクからパブリックIP を確認してブラウザでアクセスする。いつもどおりセキュリティグループでポート3001の許可も行う。今回ホスト名だけのアクセスで「Hello World」表示となるよう仕込んであり、見事に表示された。

buildspec 修正

ECS へのデプロイを行うため、buildspec.yml のコンテナ名箇所を NodeContainerTest、artifacts ファイル名を imagedefinitions.json に修正して Push する。

AWS接続環境:~/CodeCommitTest/buildspec.yml
・・・
  post_build:
      ・・・
      ###- printf '[{"name":"<container-definition>","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > artifacts.json
      - printf '[{"name":"NodeContainerTest","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imagedefinitions.json

artifacts:
  ###files: artifacts.json
  files: imagedefinitions.json
~/NodeCommitTest$ git add buildspec.yml
~/NodeCommitTest$ git commit -m "container name and artifacts filename"
[master 2f12794] container name and artifacts filename
 1 file changed, 4 insertions(+), 2 deletions(-)
~/NodeCommitTest$ git push
・・・
   389101b..2f12794  master -> master

CodePipeline 修正(デプロイ追加)

Build ステージの後に Deploy ステージを追加する。

Deploy ステージのアクショングループを追加する。

アクション名を「Deploy」、アクションプロバイダーを「Amazon ECS」、リージョンを「アジアパシフィック(東京)」、入力アーティファクトを「BuildArtifact」、クラスター名を「NodeClusterTest]、サービス名を「NodeTaskServiceTest」とする。

「保存する」ボタンを押下する。

メッセージを修正したソースを Push する。

~/NodeCommitTest$ git add src/app.ts
~/NodeCommitTest$ git commit -m "modify routing /"
・・・
 1 file changed, 1 insertion(+), 1 deletion(-)
~/NodeCommitTest$ git push
・・・
   64f18ea..24499e5  master -> master

ファイルの変更を検知し、ビルド/デプロイが自動的に走り出し、しばらく待つと完了する。

デプロイが完了して、先ほどの URL を更新しても表示されない。よく考えたらサービスが更新されたからタスクの IP も変わったわけだ!再度サービスのタスクからパブリックIP を確認してブラウザでアクセスする。

ソースを Push することで自動的にデプロイまで行くのは理解できたが、やはり本番環境では動作確認がしたいので Blue-Green Deployment というのも試してみたい(有無を言わさずデプロイする今回のがローリングアップデート)。

Blue-Green Deployment 用設定ファイル作成

パクリ元2つ目を参考に設定系ファイルを作成、修正して Push する。

AWS接続環境:~/CodeCommitTest/taskdef.json
{
    "taskRoleArn": "<TASK_ROLE_ARN>",
    "executionRoleArn": "<EXECUTION_ROLE_ARN>",
    "containerDefinitions": [
        {
            "name": "<CONTAINER_NAME>",
            "image": "<IMAGE1_NAME>",
            "essential": true,
            "portMappings": [
                {
                    "hostPort": 80,
                    "protocol": "tcp",
                    "containerPort": 80
                }
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512",
    "family": "<TASK_FAMILY>"
}
AWS接続環境:~/CodeCommitTest/appspec.yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "<TASK_DEFINITION>"
        LoadBalancerInfo:
          ContainerName: "<CONTAINER_NAME>"
          ContainerPort: 80
        PlatformVersion: "1.4.0"
AWS接続環境:~/CodeCommitTest/buildspec.yml
version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - 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
      # Docker Hub へのログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKER_HUB_PASS | docker login -u $DOCKER_HUB_USER --password-stdin
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG  --target runner .
      - echo docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      ###- printf '[{"name":"<container-definition>","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > artifacts.json
      ####- printf '[{"name":"NodeContainerTest","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imagedefinitions.json
      - printf '[{"imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json
      - sed -i -e "s#<TASK_FAMILY>#$TASK_FAMILY#" taskdef.json
      - sed -i -e "s#<TASK_ROLE_ARN>#$TASK_ROLE_ARN#" taskdef.json
      - sed -i -e "s#<EXECUTION_ROLE_ARN>#$EXECUTION_ROLE_ARN#" taskdef.json
      - sed -i -e "s#<CONTAINER_NAME>#$CONTAINER_NAME#" taskdef.json
      - sed -i -e "s#<CONTAINER_NAME>#$CONTAINER_NAME#" appspec.yaml

artifacts:
  ###files: artifacts.json
  ####files: imagedefinitions.json
    files:
      - imageDetail.json
      - taskdef.json
      - appspec.yaml
~/NodeCommitTest$ git add *
~/NodeCommitTest$ git commit -m "blue-green deployment"
[master e3f05f9] blue-green deployment
 3 files changed, 48 insertions(+), 3 deletions(-)
・・・
~/NodeCommitTest$ git push
・・・
   24499e5..e3f05f9  master -> master

buildspec.yml で出てきた「TASK_ROLE_ARN」「EXECUTION_ROLE_ARN」「CONTAINER_NAME」「TASK_FAMILY」は CodeBuild の 環境変数として定義しておく。タスクロールは設定していないから空白でいいのかね、、

ロール作成

タスク実行ロールまたはタスクロール上書きに対する iam:PassRole アクセス許可を、CodeDeploy用のIAMロールにインラインポリシーとして追加


Fargateの場合、タスク実行ロールが追加されていると思いますので、2つめの手順も忘れずに実行してください。

詳細な手順については、「Amazon ECS 開発者ガイド」のAmazon ECS CodeDeploy IAM Role を参照して下さい。

パクリ元3つ目を参考に上記からやる。「passForBlueGreenDeployment」ポリシーを作成。

「codedeploy-CodeDeployTest-service-role」ロールを作る。

先ほど作成した「passForBlueGreenDeployment」ポリシーをcodedeploy-CodeDeployTest-service-role」ロールに追加。

ALB 作成

パクリ元3つ目を参考に手順を確認して ALB 作成。以前やっているので問題なし。まずは「TargetBlue」ターゲットグループ作成。

次にロードバランサー ALB「NodeLbTest」作成。

ECS サービス作成

ローリングアップデートで試した「NodeTaskServiceTest」は削除して新たに「NodeTaskBlueGreenDeployServiceTest」作成。パクリ元3つ目通りに設定。

ALB 確認

新しくターゲットグループ TargetGreen が出来上がっていることを確認。

CodeDeploy 確認&修正

サービスは立ち上がってるのにデプロイ進行中が15分たっても消えない、、というのを我慢して何度も繰り返してサービスが正常にデプロイされた後、デプロイ設定を10分後再ルーティングに変更した。イヤ耐えたね。

CodePipeline 修正

デプロイステージのアクションを、新たなデプロイグループへ変更する。最後に「保存する」押下を忘れない。

Deploy エラー

どうも直してもよく分からんけどエラーが出る。

「The deployment failed because the AppSpec file that specifies the deployment configuration is missing or has an invalid configuration. Could not parse or validate one of the resources in the app-spec.」検索してもよく分からず。どうやら appspec.yaml の <TASK_DEFINITION> が変換されてない様子。タスク定義の ARN を直接入れると動き出すことを確認。。なぜオレのパイプラインは変換してくれない!今回のハマり場。

強引に解決

クタクタになった後、自動変換はあきらめてパクリ元4つ目を参考に強引変換に切り替え。ECSタスク定義を手動で読み込むための権限(AmazonECS_FullAccess)を CodeBuild のロール「codebuild-NodeBuildTest-service-role」に追加して buildspec.yml を修正。

AWS接続環境:~/CodeCommitTest/buildspec.yml
・・・
  post_build:
    commands:
・・・
      - TASK_DEFINITION=$(aws ecs list-task-definitions --family-prefix "${TASK_FAMILY}" --query "reverse(taskDefinitionArns)[0]" --output text)
      - sed -i -e "s#<TASK_DEFINITION>#${TASK_DEFINITION}#" appspec.yaml
・・・

やっと動き出した。ECS 権限が原因でないことも一応確認。

ロードバランサー含めポートが 80 に統一されているようなので、Dockerfile にて公開するポートを 80 にしてビルドしなおし。セキュリティグループの許可ポートも 80 に直して、ALB の DNS名でアクセス。

表示メッセージを変えて Push して デプロイ成功後すぐは元のターゲットグループ「TargetBlue」に100%つながるため表示も変わらない。

自動的にターゲットグループ「TargetGreen」に切り替わる10分後以降。

表示も切り替わる。

後片付け

セキュリティグループのルール削除、ならびにECS サービスのタスクの数」を「0」にすることで費用が掛からないように対応。

雑感

CodeDeploy による手動デプロイだとうまくいくところが CodePipeline 経由だとハマったりとか自分の環境だとうまくいかないだとか、いまいち自分の思い通りにならない箇所で手間取ると歯がゆい。

翌日、「AWS Free Tier usage limit alerting via AWS Budgets」というメールが届き、CodeBuild と CodePipeline 使いすぎ、無料枠終わったから、とのこと。テストもマイグレーションもまだなのに。ということでこの件は来月に持ち越し。その時これまでの事を思い出すために2週間ぐらいかかるかもしれん。