電通総研 テックブログ

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

社名変更に伴う Web アプリのドメイン変更が CDK でサクッと終わった話

こんにちは。コーポレート本部 サイバーセキュリティ推進部の耿です。

当社は2024年1月に社名が「電通国際情報サービス」(ISID)から「電通総研」に変わりました。
当然、各種システムの変更も社名変更に合わせて行われました。
今回は AWS CDK を利用して構築したある社内向け Web アプリのドメインを切り替える際に、意外とサクッと終わった話をしたいと思います。
ドメインの切り替えは緊張感のある作業ですが、結果的に本番環境での実作業時間はわずか30分で終わりました。

アプリのインフラ構成

今回ドメイン切り替えを行った社内向け Web アプリの大雑把なインフラ構成は次の図の通りです。関連するコンポーネントのみを抜き出しており、DB や WAF などは省略しています。また実際は ECS アプリケーションのブルーグリーンデプロイを行っているのですが、これも省略しています。
cool-app.isid.example ドメインを利用し、ALB のターゲットグループとして ECS サービスを登録しています。

アプリのインフラ構成

このWeb アプリのドメインcool-app.dentsusoken.example に切り替えます。

切り替え手順

事前準備作業と、本番切り替え作業の2段階に分けてドメイン切り替えを実施しました。

事前準備作業として以下を行います。

ドメインはこの段階ではリダイレクト用ALBを指しており、リダイレクト用ALBは旧ドメインに302リダイレクトします。
この時点でアプリケーションへの従来のアクセスには何も影響はありません。

事前準備作業

続いて本番切り替え作業では、アプリケーション用ALBのドメインを新ドメインに変更し、旧ドメインはリダイレクト用ALBを指すように切り替えます。
リダイレクト用ALBも新ドメインに301リダイレクトするように変更します。
これによりブラウザによる旧ドメインへのアクセスは全て新ドメインにリダイレクトされるようになります。ところでALBのURLリダイレクト機能は元のパス、クエリパラメータを保持したままリダイレクトを実現できるので、ほぼユーザーが意識することなくドメイン切り替えを実現できるのは嬉しいです。

本番切り替え作業

CDKによる切り替え作業: 切り替え前

ドメイン切り替え前は、以下のサンプルコードのような構成でCDKを実装していました。

アプリケーション

new MyAppStack(app, "MyAppStack", {
  hostedZoneId: "Z1111111OLDHOSTEDZONE",
  hostedZoneName: "isid.example",
  webAppRecordName: "cool-app",
});

スタック(一部のプロパティのみ掲載)

// アプリケーション用ALB
const alb = new elb.ApplicationLoadBalancer(this, "Alb", {
    vpc,
    vpcSubnets: { subnetGroupName },
    securityGroup,
    internetFacing: true,
});

// ターゲットグループ
const targetGroup = new elb.ApplicationTargetGroup(this, "TargetGroup", {
    vpc,
    port: 3000,
    protocol: elb.ApplicationProtocol.HTTP,
    targetType: elb.TargetType.IP,
});

// ホストゾーン
const hostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, "HostedZone", {
    hostedZoneId: props.hostedZoneId,
    zoneName: props.hostedZoneName,
});

// アプリケーション用ALBへのAレコード
new route53.ARecord(this, "WebARecord", {
    zone: hostedZone,
    recordName: props.webAppRecordName,
    target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)),
});

// ACM証明書
const certificate = new certificatemanager.Certificate(this, "Certificate", {
    domainName: `${props.webAppRecordName}.${props.hostedZoneName}`,
    validation: certificatemanager.CertificateValidation.fromDns(hostedZone),
})

// ALBリスナー
alb.addListener("ApplicationListener", {
    protocol: elb.ApplicationProtocol.HTTPS,
    port: 443,
    certificates: [certificate],
    sslPolicy: elb.SslPolicy.TLS13_RES,
    defaultTargetGroups: [targetGroup],
    open: false,
});

CDKによる切り替え作業: 事前準備

事前準備段階として、リダイレクト用ALB、新ドメインのAレコード、ACM証明書などを新規に作成します。
CDKのコード上ではプロパティ名を変更し、新旧どちらのドメインを使用しているのか明確に分かるようにしました。

アプリケーション

new MyAppStack(app, "MyAppStack", {
  // 旧ホストゾーンのプロパティ名を変更
  oldHostedZoneId: "Z1111111OLDHOSTEDZONE",
  oldHostedZoneName: "isid.example",
  // 新ホストゾーンのプロパティを追加
  newHostedZoneId: "Z2222222NEWHOSTEDZONE",
  newHostedZoneName: "dentsusoken.example",
  webAppRecordName: "cool-app",
});

スタック

// 【変更なし】アプリケーション用ALB
const alb = new elb.ApplicationLoadBalancer(this, "Alb", {
    vpc,
    vpcSubnets: { subnetGroupName },
    securityGroup,
    internetFacing: true,
});

// 【追加】リダイレクト用ALB
// ドメイン切り替え前は新ドメインから旧ドメインへ、
// ドメイン切り替え後は旧ドメインから新ドメインへリダイレクトする
const redirectAlb = new elb.ApplicationLoadBalancer(this, "RedirectAlb", {
    vpc,
    vpcSubnets: { subnetGroupName },
    securityGroup,
    internetFacing: true,
});

// 【変更なし】ターゲットグループ
const targetGroup = new elb.ApplicationTargetGroup(this, "TargetGroup", {
    vpc,
    port: 3000,
    protocol: elb.ApplicationProtocol.HTTP,
    targetType: elb.TargetType.IP,
});

// 【変数名変更】旧ホストゾーン
const oldHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, "HostedZone", {
    hostedZoneId: props.oldHostedZoneId,
    zoneName: props.oldHostedZoneName,
});

// 【追加】新ドメインのホストゾーン
const newHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, "NewHostedZone", {
    hostedZoneId: props.newHostedZoneId,
    zoneName: props.newHostedZoneName,
});

// 【変数名変更】旧ドメインのAレコード
new route53.ARecord(this, "WebARecord", {
    zone: oldHostedZone,
    recordName: props.webAppRecordName,
    target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)),
});

// 【追加】新ドメインのAレコード
new route53.ARecord(this, "NewWebARecord", {
    zone: newHostedZone,
    recordName: props.webAppRecordName,
    target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(redirectAlb)),
});

// 【変数名変更】旧ドメインのACM証明書
const oldCertificate = new certificatemanager.Certificate(this, "Certificate", {
    domainName: `${props.webAppRecordName}.${props.oldHostedZoneName}`,
    validation: certificatemanager.CertificateValidation.fromDns(oldHostedZone),
})

// 【追加】新ドメインのACM証明書
const newCertificate = new certificatemanager.Certificate(this, "NewCertificate", {
    domainName: `${props.webAppRecordName}.${props.newHostedZoneName}`,
    validation: certificatemanager.CertificateValidation.fromDns(newHostedZone),
})

// 【変数名変更】アプリケーション用ALBのリスナー
alb.addListener("ApplicationListener", {
    protocol: elb.ApplicationProtocol.HTTPS,
    port: 443,
    certificates: [oldCertificate],
    sslPolicy: elb.SslPolicy.TLS13_RES,
    defaultTargetGroups: [targetGroup],
    open: false,
});

// 【追加】リダイレクト用ALBのリスナー
redirectAlb.addListener("RedirectAlbListener", {
    protocol: elb.ApplicationProtocol.HTTPS,
    port: 443,
    certificates: [newCertificate],
    sslPolicy: elb.SslPolicy.TLS13_RES,
    open: false,
    // ALB の機能で旧ドメインへ 302 リダイレクト
    defaultAction: elb.ListenerAction.redirect({
        host: `${props.webAppRecordName}.${props.oldHostedZoneName}`,
        permanent: false,
    }),
});

CDKによる切り替え作業: 本番切り替え

ドメイン切り替え本番では、ホストゾーンのレコードの指す先と証明書がアタッチされるALBを変更し、リダイレクト用ALBのリダイレクト先ドメインも変更しました。

スタック

// 【変更なし】アプリケーション用ALB
const alb = new elb.ApplicationLoadBalancer(this, "Alb", {
    vpc,
    vpcSubnets: { subnetGroupName },
    securityGroup,
    internetFacing: true,
});

// 【変更なし】リダイレクト用ALB
const redirectAlb = new elb.ApplicationLoadBalancer(this, "RedirectAlb", {
    vpc,
    vpcSubnets: { subnetGroupName },
    securityGroup,
    internetFacing: true,
});

// 【変更なし】ターゲットグループ
const targetGroup = new elb.ApplicationTargetGroup(this, "TargetGroup", {
    vpc,
    port: 3000,
    protocol: elb.ApplicationProtocol.HTTP,
    targetType: elb.TargetType.IP,
});

// 【変更なし】旧ホストゾーン
const oldHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, "HostedZone", {
    hostedZoneId: props.oldHostedZoneId,
    zoneName: props.oldHostedZoneName,
});

// 【変更なし】新ドメインのホストゾーン
const newHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes(this, "NewHostedZone", {
    hostedZoneId: props.newHostedZoneId,
    zoneName: props.newHostedZoneName,
});

// 【変更】旧ドメインのAレコード
new route53.ARecord(this, "WebARecord", {
    zone: oldHostedZone,
    recordName: props.webAppRecordName,
    // 旧ドメインはリダイレクト用ALBをターゲットに転送
    target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(redirectAlb)),
});

// 【変更】新ドメインのAレコード
new route53.ARecord(this, "NewWebARecord", {
    zone: newHostedZone,
    recordName: props.webAppRecordName,
    // 新ドメインはアプリケーション用ALBをターゲットに転送
    target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)),
});

// 【変更なし】旧ドメインのACM証明書
const oldCertificate = new certificatemanager.Certificate(this, "Certificate", {
    domainName: `${props.webAppRecordName}.${props.oldHostedZoneName}`,
    validation: certificatemanager.CertificateValidation.fromDns(oldHostedZone),
})

// 【変更なし】新ドメインのACM証明書
const newCertificate = new certificatemanager.Certificate(this, "NewCertificate", {
    domainName: `${props.webAppRecordName}.${props.newHostedZoneName}`,
    validation: certificatemanager.CertificateValidation.fromDns(newHostedZone),
})

// 【変更】アプリケーション用ALBのリスナー
alb.addListener("ApplicationListener", {
    protocol: elb.ApplicationProtocol.HTTPS,
    port: 443,
    // 新ドメインの証明書に変更
    certificates: [newCertificate],
    sslPolicy: elb.SslPolicy.TLS13_RES,
    defaultTargetGroups: [targetGroup],
    open: false,
});

// 【変更】リダイレクト用ALBのリスナー
redirectAlb.addListener("RedirectAlbListener", {
    protocol: elb.ApplicationProtocol.HTTPS,
    port: 443,
    // 旧ドメインの証明書に変更
    certificates: [oldCertificate],
    sslPolicy: elb.SslPolicy.TLS13_RES,
    open: false,
    // ALB の機能で新ドメインへ 301 リダイレクト
    defaultAction: elb.ListenerAction.redirect({
        host: `${props.webAppRecordName}.${props.newHostedZoneName}`,
        permanent: true,
    }),
});

以上で難なくドメイン切り替え作業が完了しました。あとはDNSのキャッシュが切れたら新ドメインでアプリにアクセスできるようになります。
本番切り替え作業にかかった時間はCDKのデプロイから切り替え後の接続確認まで含めても30分ほどでした。(実際はもっと色々なインフラリソースに関わる作業があったので、本記事に記載した構成だけならもっと早く終わっていたと思います)

所感

これまでの内容では省いていますが、各作業段階においてもちろんステージング環境での検証が重要です。
IaCでインフラを管理することのメリットは、ステージング環境で検証したことをその通りに本番環境にも適用できることです。
特にCDKでは旧ドメインが使われている場所を、コードエディタで簡単に洗い出し、変数として辿れるのが便利だと改めて思いました。
そもそも複数環境を構築するときに同じスタックを使い回す前提で実装することで、実装段階から環境に固有の変数(ドメイン名が典型的な例ですね)を意識して切り出すような書き方になります。既にそのような書き方がされているCDKアプリにおいて、ドメインを変更する作業が簡単に終わったのも当然といえるかもしれません。

CDK最高です。

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