[CDK] 内部 ALB に AWS Certificate Manager でサーバ証明書を発行してみた

[CDK] 内部 ALB に AWS Certificate Manager でサーバ証明書を発行してみた

インターネットに公開しない内部ALBへのサーバ証明書の発行をスプリットビュー DNS 構成を用いた構成で実装してみました
2025.04.01

製造ビジネステクノロジー部の新澤です。

インターネットに公開しないプライベート VPC 内で ELB や API Gateway のプライベート REST API を利用する際、 HTTPS で通信するために独自ドメインで証明書を発行したいというユースケースで、「プライベート CA を作るのは運用の手間や利用費が気になる」といった場合に選択できる方法として、 Route53 でパブリックホストゾーンとプライベートホストゾーンで同じドメイン名を使用するスプリットビュー DNS を使う方法があります。

今回は、スプリットビュー DNS 構成を用いた証明書の発行を AWS CDK で実装してみます。

※ スプリットビュー DNS に関しては以下が詳しいです。

【Route53】スプリットビューDNSの名前解決順序を整理してみた | DevelopersIO

構成図

今回の構成では、下図のようにプライベートサブネットに配置された 内部ALB に対して、独自ドメインで HTTPS 通信を行えるようにします。

なお、前提条件として、パブリックホストゾーンは既に存在しているものを利用する想定としています。

architecture.drawio

実装してみる

プライベートホストゾーン

パブリックホストゾーンのサブドメインとなるようにプライベートホストゾーンを作成します。(パブリックホストゾーンと同一ドメイン名でも機能しますが、わかりやすくするためサブドメインとしています)

パブリックホストゾーンは、ACM でサーバ証明書発行する時の DNS 検証を行う際に CNAME レコードを登録するために利用します。

また、プライベートホストゾーンは、 ALB のエイリアスレコードを登録するために利用します。

const publicHostedZone = route53.HostedZone.fromLookup(this, 'PublicHostedZone', {
  domainName: props.domainName,
  privateZone: false,
});
const privateHostedZone = new route53.PrivateHostedZone(this, 'PrivateHostedZone', {
  vpc,
  zoneName: `private.${props.domainName}`,
});

サーバ証明書の発行

AWS Certificate Managerでサーバ証明書を発行します。

パブリックホストゾーンで DNS 検証を行うように設定します。

const certificate = new acm.Certificate(this, 'Certificate', {
  domainName: `api.${privateHostedZone.zoneName}`,
  validation: acm.CertificateValidation.fromDns(publicHostedZone),
});

エイリアスレコード

プライベートホストゾーンに Fargate サービスの ALB に対するエイリアスレコードを登録します。

new route53.ARecord(this, 'AliasRecord', {
  zone: privateHostedZone,
  recordName: `api.${privateHostedZone.zoneName}`,
  target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(service.loadBalancer)),
});

CDK コード全体

以下が作成したスタック全体のコードになります。

なお、ApplicationLoadBalancedFargateServiceだけでプライベートホストゾーンへのエイリアスレコードの登録まで可能なのですが、今回は説明のためあえてARecordを分けて実装しています。

コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  aws_route53 as route53,
  aws_route53_targets as route53targets,
  aws_certificatemanager as acm, aws_ec2 as ec2,
  aws_elasticloadbalancingv2 as elbv2,
  aws_ecs as ecs,
  aws_ecs_patterns as ecsPatterns
} from 'aws-cdk-lib';

export interface InternalAlbCertificateStackProps extends cdk.StackProps {
  readonly domainName: string;
}

export class InternalAlbCertificateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: InternalAlbCertificateStackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'Vpc', {
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ]    
    });

    const publicHostedZone = route53.HostedZone.fromLookup(this, 'PublicHostedZone', {
      domainName: props.domainName,
      privateZone: false,
    });
    const privateHostedZone = new route53.PrivateHostedZone(this, 'PrivateHostedZone', {
      vpc,
      zoneName: `private.${props.domainName}`,
    });

    const certificate = new acm.Certificate(this, 'Certificate', {
      domainName: `api.${privateHostedZone.zoneName}`,
      validation: acm.CertificateValidation.fromDns(publicHostedZone),
    });

    const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'SampleService', {
      vpc: vpc,
      cpu: 256,
      memoryLimitMiB: 512,
      desiredCount: 1,
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:1.27'),
        containerPort: 80,
      },
      publicLoadBalancer: false,
      protocol: elbv2.ApplicationProtocol.HTTPS,
      certificate: certificate,
      loadBalancerName: 'SampleLoadBalancer',
      // ↓ 以下でもエイリアスレコードを登録可能
      // domainName: `api.${privateHostedZone.zoneName}`,
      // domainZone: privateHostedZone,
    });

    new route53.ARecord(this, 'AliasRecord', {
      zone: privateHostedZone,
      recordName: `api.${privateHostedZone.zoneName}`,
      target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(service.loadBalancer)),
    });

    const bastion = new ec2.BastionHostLinux(this, 'BastionHost', {
      vpc,
      instanceName: 'BastionHost',
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
      machineImage: ec2.MachineImage.latestAmazonLinux2023({
        cpuType: ec2.AmazonLinuxCpuType.ARM_64,
      }),
      subnetSelection: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      requireImdsv2: true,
    });
    service.service.connections.allowFrom(bastion, ec2.Port.HTTPS, 'Allow HTTP traffic from Bastion Host');
  }
}

デプロイして動作確認

デプロイ後にBastion Hostに接続して、 ALB にリクエストしてみます。

[ssm-user@ip-10-0-31-139 bin]$ curl https://api.private.${ドメイン名}/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

内部 ALB に、独自ドメインで HTTPS リクエストできました!

おわりに

今回のようなユースケースの場合、AWS Private CA の利用も選択肢になりますが、プライベート CA 当たり月額400 USD が発生するため、採用に二の足を踏む場面もあるのではないでしょうか?(私はあります)

今回のようにスプリットビュー DNS を利用することで、利用費を抑えることが可能になります。

また、今回ご紹介した ALB だけでなく API Gateway のプライベート REST API でも利用可能ですので、活用の幅も広いかと思います。

どなたかのお役に立てれば幸いです。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.