電通総研 テックブログ

電通総研が運営する技術ブログ

NLBとECS でサービス公開 ハマりポイント解説

こんにちは。電通総研コーポレート本部システム推進部の山下です。

AWSでサービスを構築していると、固定IPでサービスを公開したい場合があります。 DNSなどのIPで通信先を指定するようなサービスでは、AWSではNetwork LoadBalancer(NLB)を利用するのが一般的です。 本記事では、AWS Cloud Development Kit (CDK) を使ってElastic Container Service (ECS) とNLBを組み合わせてサービスを公開する方法について紹介します。

CDKのNetworkLoadBalancedFargateServiceを使ってサービスを公開する

CDKの NetworkLoadBalancedFargateService を利用すると、簡単にNLBとFargateを組み合わせたサービスの構築が出来ます。

しかし、以下のような形でこの関数だけを呼び出して cdk deploy を実施すると失敗します。

const loadBalancedFargateService = new NetworkLoadBalancedFargateService(this, "Service", {
  memoryLimitMiB: 1024,
  cpu: 512,
  taskImageOptions: {
    image: ecs.ContainerImage.fromRegistry("nginx")
  },
});

これは、 NetworkLoadBalancedFargateService で構築されたFargateにはSecurityGroupが適切に設定されておらず NLBからのヘルスチェックに失敗してしまうため cdk deploy にも失敗してしまうためです。

例えば、以下のような記述を追加して特定のポートに対するアクセスを許可する必要があります。

const myService = loadBalancedFargateService.service;
const mySg = myService.connections.securityGroups[0];
mySg.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(80), 'SSH frm anywhere');

これで、NLBからのヘルスチェックが動くようになり cdk deploy が成功します。

UDPのサービスをNLBで公開する

NLBでUDPのサービスを公開するには、NLBのヘルスチェックはTCPを使った通信で実施されるということに注意する必要があります。 つまり、UDPのサービス単体ではNLBのヘルスチェックに応答することが出来ず公開できないということになります。

ECSからサービスを公開する場合にもこれは同様です。 ECSのServiceに2つのタスクを起動し片方をヘルスチェック用として公開するなどの工夫を行う必要があります。

例えば以下のように、UDPでサービスを公開するタスクのタスク定義を作ります。

// ECS Clusterの作成
const cluster = new ecs.Cluster(this, 'udp-service-cluster', {
  vpc: vpc // 事前に作成してあるVPC
});

const taskDef = new ecs.FargateTaskDefinition(this, "udp-task", {
  family: 'udp-task',
  memoryLimitMiB: 512,
  cpu: 256,
  executionRole: execRole, // 事前に定義してあるRole
  taskRole: taskRole // 事前に定義してあるRole
});

const repository = Repository.fromRepositoryName(this, "udp-service-registry", 'udp-service');
taskDef.addContainer("udp-service-container", {
  containerName: "udp-service",
  image: ecs.ContainerImage.fromEcrRepository(repository, 'latest'),
  portMappings: [{
    containerPort: 1234,
    protocol: ecs.Protocol.TCP
  }]
})

// CDK上でUDPしか公開していないタスク定義はエラーになるので以下のように addPropertyOverrideで対応する
const td = taskDef.node.defaultChild as ecs.CfnTaskDefinition;
td.addPropertyOverride('ContainerDefinitions.0.PortMappings.0.Protocol', 'udp');

これで、UDPだけを公開するタスク定義は作成できます。 これに加えて、ヘルスチェック用に追加でコンテナを稼働させるように変更します。

ここでは、以下のようなシェルのワンライナーで動作するような簡易サーバを動作させてヘルスチェックに応答させることにします。 8080番ポートで動作し、'ok' と返すだけのサーバ(もどき)です。 これでは実際のサービスのヘルスチェックにはなりません。あくまでNLBからのヘルスチェックに応答させるためのものです。 死活監視は別の方法で担保する必要があることに注意してください。

const healthCheckcontainer = taskDef.addContainer("healthcheck-container", {
  image: ecs.ContainerImage.fromRegistry("busybox:latest"),
  entryPoint: ["sh", "-c"],
  command: [
    "while true; do { echo -e 'HTTP/1.1 200 OK\r\n'; echo 'ok'; } | nc -l -p 8080; done"
  ]
})
healthCheckcontainer.addPortMappings({
  containerPort: 8080,
});

そして、このタスク定義を使ったFargateServiceを作ります。 アクセスを許可するためのセキュリティグループも合わせて作ります。

const secGroup = new ec2.SecurityGroup(this, 'ntp-sg', {
  vpc: vpc,
  allowAllOutbound: true
});
secGroup.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.udp(1234), 'for UDP service');
secGroup.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(8080), 'for healthCheck');

const fargateService = new cdk.aws_ecs.FargateService(this, 'udp-service', {
  cluster,
  taskDefinition: taskDef,
  assignPublicIp: false,
  securityGroups: [secGroup],
  enableExecuteCommand: true,
});

ここまでで、UDPサービスを公開するFargateは完成しました。

あとは、公開に利用するNLBの構築、ターゲットグループなどの設定を行う必要があります。

// NLB を作成する
const nlb = new NetworkLoadBalancer(this, 'udp-service-nlb', {
  internetFacing: false, // 今回は内部向けサービスを想定
  vpc: vpc,
  crossZoneEnabled: false,
  ipAddressType: IpAddressType.IPV4,
});

const listener = nlb.addListener('udp-service-listener', {
  port: 1234,
  protocol: lb.Protocol.UDP
});

listener.addTargets('udp-service-tg', {
  port: 1234,
  protocol: lb.Protocol.UDP,
  targets: [fargateService.loadBalancerTarget({
    containerName: "udp-service",
    containerPort: 1234
  })],
  deregistrationDelay: Duration.seconds(300),
  // 作成したヘルスチェック用のコンテナに通信するよう設定
  healthCheck: {
    port: "8080",
    protocol: lb.Protocol.TCP
  }
});

これで、NLBと作成したFargateの連携ができるようになりました。 ヘルスチェック向けのコンテナ追加などの手間は増えてしまいますが、 NLBと連携させることで可用性や冗長性の確保、IPの固定などが可能になりました。

まとめ

今回はNLBとECSの組み合わせでサービスを公開する際のハマりポイントを紹介してみました。
注意点をまとめますと

  • セキュリティグループの設定を実施しないとヘルスチェックに失敗する
  • UDPのサービスを公開する場合でもTCPでヘルスチェックは実施される

この2点となります。
もし、NLBを使ってサービス公開する場面があったら本記事が参考になれば幸いです。

執筆:@yamashita.tsuyoshi、レビュー:@kinjo.ryuki
Shodoで執筆されました