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

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

インターネットに公開しない内部ALBへのサーバ証明書の発行をスプリットビュー DNS 構成を用いた構成で実装してみました
Clock Icon2025.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 でも利用可能ですので、活用の幅も広いかと思います。

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

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.