CodeDeployのVPCエンドポイントが必要なケースを整理してみた

2022.09.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、八木です。
CodeDeployを使ってサービスをデプロイする際、どんな時にCodeDeployのVPCエンドポイントが必要なのか混乱したため、調べてみました。

まず前提として、インターネットへのアクセスがない(NAT Gateway等へのルートが存在しない)プライベートサブネットへのリソースのデプロイを想定しています。
インターネット接続が可能な環境では、VPCエンドポイントは必要ありません。

VPCエンドポイントが必要になる場合

VPCエンドポイントが必要になるかは、デプロイするサービスによって異なります。CodeDeployがデプロイ可能なサービスは「EC2(またはオンプレ)」「Lambda」「ECS」です。

デプロイ対象がEC2(またはオンプレ)の場合

CodeDeployからEC2へデプロイする場合は、あらかじめEC2にCodeDeployエージェントをインストールしておく必要があります。このエージェントがCodeDeployと通信を行い、appspec.yamlのHooksの項目に設定しているコマンドを実行し、デプロイを行います。このため、CodeDeployサービスエンドポイントへの通信経路(VPCエンドポイントまたはNAT)が必要になります。
必要なエンドポイントは com.amazonaws.ap-northeast-1.codedeploycom.amazonaws.region.codedeploy-commands-secure の2つです。

デプロイ対象がLambdaまたはECSの場合

LambdaもしくはECSの場合は、基本的にVPCからCodeDeployサービスへの通信経路は必要ありません。

まずLambdaのデプロイは、Lambda関数エイリアスの示すバージョンを変更するという内容です。このため、通常のLambda関数でも、VPC Lambda関数でも、VPC内からの通信は行われません。

続いてECSのデプロイですが、こちらもVPC内からCodeDeployへの通信は行われません。プロバイダがFargate、EC2どちらの場合でもです。
CodeDeployからのECSデプロイは、主にECSタスクの作成/削除、ターゲットグループへのタスク登録/登録解除、ロードバランサのリスナー変更を行います。これらはCodeDeployサービスからECS/EC2サービスエンドポイントへの通信であるため、CodeDeployのVPCエンドポイントは必要ありません。

ただし、LambdaまたはECSのデプロイの際、ライフサイクルイベントフックにVPC Lambdaを設定している場合には、通信経路が必要になります。
ライフサイクルイベントフックとは、アプリケーションのインストール前後やトラフィックを許可する前後で、カスタマイズしたアクションを実行する機能です。この機能を用いることで、デプロイ前後にサービスが想定通りに動いているかなどを検証することができます。
LambdaまたはECSのデプロイの場合、ライフサイクルフックイベントにはLambda関数を指定します。このLambda関数をVPC内で動かしている場合は、アクションの結果をCodeDeployへ送信するためにVPCエンドポイントが必要になります。
この場合、必要なエンドポイントは com.amazonaws.ap-northeast-1.codedeploy のみです。

ライフサイクルイベントフックでVPC Lambdaを使ってみた

CodeDeployのVPCエンドポイントが必要な一例として、ライフサイクルイベントフックにVPC Lambdaを用いるケースを試してみます。

事前準備

事前準備として、デプロイ先となるECS環境を作成します。
まずECS上で動かすDockerイメージをECRにアップロードします。
ECRリポジトリを作成し、

aws ecr create-repository --repository-name demo-web-server

作成したリポジトリにDockerログインします。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

続いて以下のDockerfileからイメージを作成し、ECRにプッシュします。

FROM httpd:2.4
CMD [ "/bin/sh", "-c", "echo 'Hello World!' >  /usr/local/apache2/htdocs/index.html && httpd-foreground" ]
docker buildx build -t 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/demo-web-server:latest --push --platform linux/amd64 .

私のローカル環境はMac M1であり、Fargate(x86_64)で動くイメージを作成するため、 docker buildx を使用しました。x86_64ベースのPCをお使いの方は、 docker buildでも大丈夫です。
docker buildxについて詳しく知りたい方は、公式ドキュメントや以下の記事をご覧ください。

作成したコンテナは Hello World! という文字列を返してくれる、単純なWebサーバです。

続いてVPCエンドポイントの作成です。
プライベートサブネットに以下4つのVPCエンドポイントを作成します。

  • com.amazonaws.ap-northeast-1.ecr.dkr
  • com.amazonaws.ap-northeast-1.ecr.api
  • com.amazonaws.ap-northeast-1.s3 (※ゲートウェイ型)
  • com.amazonaws.ap-northeast-1.logs

ecrおよびs3はFargateがコンテナを取得する際に必要なエンドポイントです。
logsはコンテナのログをCW Logsへ送信するためのエンドポイントです。

次にECSリソースを作成していきます。今回はプロバイダにFargateを使用します。
以下のようなタスク定義を作成します。

{
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "inferenceAccelerators": [],
    "containerDefinitions": [
        {
            "name": "web",
            "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/demo-web-server:latest",
            "resourceRequirements": null,
            "essential": true,
            "portMappings": [
                {
                    "containerPort": "80",
                    "protocol": "tcp"
                }
            ],
            "environment": null,
            "environmentFiles": [],
            "secrets": null,
            "mountPoints": null,
            "volumesFrom": null,
            "hostname": null,
            "user": null,
            "workingDirectory": null,
            "extraHosts": null,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/hello-world-server",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "ulimits": null,
            "dockerLabels": null,
            "dependsOn": null,
            "repositoryCredentials": {
                "credentialsParameter": ""
            }
        }
    ],
    "volumes": [],
    "networkMode": "awsvpc",
    "memory": "512",
    "cpu": "256",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "taskRoleArn": "",
    "family": "hello-world-server",
    "runtimePlatform": {
        "operatingSystemFamily": "LINUX"
    },
    "tags": []
}

続いて、ECSクラスターおよびサービスを作成します。
サービスの作成ではBlue/Greenデプロイメントを選択します。こうすることで、デプロイに使用するCodeDeployを一緒にセットアップしてくれます。

なお、新コンソールではBlue/Greenデプロイメントを選択できないため、旧コンソールをご利用ください。(2022/09/14時点)

確認画面では以下のようになります。

サービスの作成が完了し正常にタスクが起動されると、ALBから以下のようなレスポンスを受け取ります。

これで、デプロイ先となるECS環境がセットアップできました。

ライフサイクルイベントフックに利用するVPC Lambdaの作成

Green環境にトラフィックが移行する前に、VPC内のリソースにアクセスして検証したい場合、VPC Lambdaを使用します。
この関数をライフサイクルイベントフックの BeforeAllowTraffic に設定することで、要件を満たさない場合にはコンテナのデプロイを防ぐことができます。
今回はJavaScriptでLambda関数を作成していきます。
ランタイムをNode.jsにし、詳細設定でECSタスクをデプロイするVPCを選択します。

作成したLambda関数のコードを以下に変更します。

"use strict";

const aws = require("aws-sdk");
const codedeploy = new aws.CodeDeploy({ apiVersion: "2014-10-06" });

exports.handler = (event, context, callback) => {
  // カスタマイズした検証を行い、検証結果(失敗または成功)をCodeDeployに通知する
  // const isRDSAvailable = checkRDSConnection();
  // const checkStatus = isRDSAvailable ? "Succeeded" : "Failed";

  const checkStatus = "Succeeded";

  const params = {
    deploymentId: event.DeploymentId,
    lifecycleEventHookExecutionId: event.LifecycleEventHookExecutionId,
    status: checkStatus, // SucceededかFailedで結果を通知
  };
  codedeploy.putLifecycleEventHookExecutionStatus(params, (err, data) => {
    if (err) {
      callback("Validation test failed");
    } else {
      callback(null, "Validation test succeeded");
    }
  });
};

続いて作成したLambda関数のIAMロールに以下のインラインポリシーを追加し、Lambda関数からCodeDeployへライフサイクルイベントフックの結果を送信できるようにします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "codedeploy:PutLifecycleEventHookExecutionStatus",
            "Resource": "*"
        }
    ]
}

CodeDeployのVPCエンドポイントの作成

作成したVPC Lambda関数からCodeDeployへの通信経路を作成します。
事前準備で作成したVPCエンドポイントに加えて、 com.amazonaws.ap-northeast-1.codedeploy のVPCエンドポイントを作成します。

デプロイしてみる

これでライフサイクルイベントフックに利用するLambdaおよび、その周辺設定が完了しました。
実際にCodeDeployでデプロイを行い、ライフサイクルイベントフックのVPC Lambdaが正常に動作することを確認してみます。

ECSサービスを作成した際、自動的にCodeDeployのデプロイメントグループが作成されています。このデプロイメントグループで「デプロイの作成」からデプロイを行なっていきます。
AppSpecに以下のYAMLを指定し、デプロイします。
TaskDefinition にはタスク定義のARN、 Hooksの BeforeAllowTraffic にはLambda関数名を指定します。

Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/hello-world-server"
        LoadBalancerInfo: 
          ContainerName: "web" 
          ContainerPort: 80
Hooks:
  - BeforeAllowTraffic: "CodeDeployHook_validateVPCResources"

デプロイが始まると以下のような画面になります。

少し時間が経つと、ライフサイクルイベントフックが実行され、デプロイが完了します。
LambdaのCWメトリクスを確認しても、Lambdaが実行されていることが確認できました。

VPCエンドポイントがないとどうなるのか

VPCエンドポイントがないとどうなるでしょうか?
試しにCodeDeployのVPCエンドポイントを削除して、再度デプロイを行なってみます。

先ほどと同じようにデプロイ進捗状況の画面になりますが、ステップ2から進まなくなります。

ページ下部の「デプロイのライフサイクルイベント」を見ると、「BeforeAllowTraffic」が進行中のまま止まってしまっています。

LambdaからCodeDeployへの通信経路が存在しないため、LambdaからCodeDeployへの結果通知が届かずに、CodeDeployが待ち続けている状態でした。

デフォルトではCodeDeployは1時間待機し続けます。VPCエンドポイントを設置していても、セキュリティグループやネットワークACLなどの設定によって通信経路がない場合は、同じ状態になります。ご注意ください。

まとめ

CodeDeployのVPCエンドポイントが必要なケースは、以下の2パターンでした。

  • デプロイ対象がEC2の場合
  • デプロイ対象がLambda/ECSで、ライフサイクルイベントフックにVPC Lambdaを利用する場合

また、必要なVPCエンドポイントはそれぞれ異なります。
想定した動作が行われない、、タイムアウトする、、といった場合はネットワーク設定を疑ってみてください。

以上、データアナリティクス事業本部コンサルティングチームの八木でした!

おまけ

ライフサイクルイベントフックからLambdaを呼び出した場合、Lambda関数のパラメータは以下のような形式でした。

{
  "DeploymentId": "d-QNZ5VKRFJ",
  "LifecycleEventHookExecutionId": "eyJlbmNyeXB0ZWREYXRhIjoibEEvemh3c2VCVVR2ci90ZVRYVm01L0V5Y09YbGoreFNWd2t1OEZNamd0ZjdVRDh2SjRVOXFSM2M4WHE5TVo2REdYa1MrZXlIeUtyRDFNOUQ3U05QaVdxa1QzaWxaUmJPSGYyaFQ3RGZQRlBPSDVLL3oxdXBkWTAzSHpBVFg5R2p5TnBOWjBvZWdnPT0iLCJpdlBhcmFtZXRlclNwZWMiOiJCSEVRdzFHMWJCUFlZdzAyIiwibWF0ZXJpYWxTZXRTZXJpYWwiOjF9"
}

コンテナのプライベートIPなどがあれば、コンテナごとにHTTPリクエストを行うなどの検証ができたのですが、残念ながらできませんでした。
コンテナの正常性確認には、ロードバランサーのテストリスナーを使用するのが良さそうです。

参考リンク

Use CodeDeploy with Amazon Virtual Private Cloud
EC2/オンプレミスの Blue/Green デプロイ用のデプロイグループを作成する (コンソール)
Amazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink)
AppSpec 「resources」セクション (Amazon ECS とAWS Lambdaデプロイメントのみ)
Amazon ECS コンピューティングプラットフォームでのデプロイ