Dans la première partie, nous avons chargé le kit de développement logiciel AWS et créé des clients de service réutilisables. Les deuxième et troisième parties utilisaient ces clients pour téléverser et modérer des images sans faire passer les données du fichier par ColdFusion.
Pour l'article final, nous allons modifier des enregistrements DNS. Cela mérite un peu plus de prudence que le téléversement d'une photo de profil. Une image brisée dérange un seul utilisateur. Un enregistrement de domaine brisé peut faire disparaître toute une application pendant que chaque serveur continue de fonctionner parfaitement. C'est une façon remarquablement efficace de gâcher un après-midi.
Notre exemple gérera des enregistrements de nom canonique sous un suffixe appartenant à l'application :
customer-name.example.comChaque enregistrement pointera vers une cible configurée :
application.example.netLe visiteur peut choisir customer-name. Il ne choisira pas la zone hébergée, le type d'enregistrement, le nom d'hôte complet ni la cible. L'entrée de l'utilisateur et la gestion illimitée des domaines ne devraient jamais être introduites. J'ai pris de mauvaises décisions architecturales, mais je préfère ne pas automatiser les conséquences.
Restreindre les autorisations Amazon
L'application a besoin de l'autorisation de :
- Modifier des enregistrements dans une zone hébergée
- Répertorier les enregistrements dans cette zone hébergée avant la suppression
- Vérifier si une modification soumise s'est propagée
Route 53 prend en charge des conditions d'autorisation qui restreignent les noms d'enregistrement, les types d'enregistrement et les actions autorisées. Nous devrions utiliser les trois.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:ChangeResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/Z11111112222222333333",
"Condition": {
"ForAllValues:StringLike": {
"route53:ChangeResourceRecordSetsNormalizedRecordNames": [
"*.sites.example.com"
]
},
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsRecordTypes": [
"CNAME"
],
"route53:ChangeResourceRecordSetsActions": [
"UPSERT",
"DELETE"
]
}
}
},
{
"Effect": "Allow",
"Action": "route53:ListResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/Z11111112222222333333"
},
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
}
]
}Remplacez l'identifiant de zone hébergée et les noms de domaine par vos propres valeurs. Ces conditions constituent une deuxième barrière autour du code. Même si l'application tente de modifier un autre type d'enregistrement ou une autre partie de la zone hébergée, Amazon rejette la demande.
Amazon documente les règles de normalisation et les restrictions disponibles dans son guide d'autorisations granulaires Route 53.
L'application devrait gérer un suffixe dédié comme sites.example.com. Ne donnez pas au code d'application courant l'autorisation de modifier les enregistrements de courrier, les serveurs de noms, les enregistrements de vérification ou le domaine racine. C'est trop de confiance pour du code que j'ai probablement écrit en train de manger le dîner.
Création du service d'enregistrements
Créez models/route53_record_service.cfc :
component output="false" {
public any function init(
required any route53_client,
required string hosted_zone_id,
required string managed_suffix,
required string canonical_name_target,
numeric time_to_live_seconds = 300
) {
variables.route53_client = arguments.route53_client;
variables.hosted_zone_id = trim( arguments.hosted_zone_id );
variables.managed_suffix = normalizeName( arguments.managed_suffix );
variables.canonical_name_target = normalizeName( arguments.canonical_name_target );
variables.time_to_live_seconds = arguments.time_to_live_seconds;
if ( !len( variables.hosted_zone_id ) ) {
throw(
type = "Route53RecordService.InvalidConfiguration",
message = "Un identifiant de zone hébergée est requis."
);
}
if ( !len( variables.managed_suffix ) || !len( variables.canonical_name_target ) ) {
throw(
type = "Route53RecordService.InvalidConfiguration",
message = "Un suffixe géré et une cible de nom canonique sont requis."
);
}
if ( variables.time_to_live_seconds < 1 ) {
throw(
type = "Route53RecordService.InvalidConfiguration",
message = "Le temps de vie doit être supérieur à zéro."
);
}
return this;
}
public struct function upsertHostname( required string label ) {
var fully_qualified_name = buildManagedName( arguments.label );
var resource_record = createObject(
"java",
"software.amazon.awssdk.services.route53.model.ResourceRecord"
)
.builder()
.value( ensureTrailingDot( variables.canonical_name_target ) )
.build();
var record_set = createObject(
"java",
"software.amazon.awssdk.services.route53.model.ResourceRecordSet"
)
.builder()
.name( fully_qualified_name )
.type(
createObject( "java", "software.amazon.awssdk.services.route53.model.RRType" ).CNAME
)
.ttl( javacast( "long", variables.time_to_live_seconds ) )
.resourceRecords( [ resource_record ] )
.build();
var result = submitChange(
action = "UPSERT",
record_set = record_set,
comment = "Provision application hostname " & fully_qualified_name
);
result.changed = true;
result.fully_qualified_name = fully_qualified_name;
return result;
}
public struct function deleteHostname( required string label ) {
var fully_qualified_name = buildManagedName( arguments.label );
var existing = findCanonicalNameRecord( fully_qualified_name );
if ( !existing.found ) {
return {
changed: false,
fully_qualified_name: fully_qualified_name,
status: "ABSENT",
change_id: ""
};
}
var values = existing.record_set.resourceRecords();
if (
values.size() != 1
|| normalizeName( values.get( 0 ).value() ) != variables.canonical_name_target
) {
throw(
type = "Route53RecordService.RecordConflict",
message = "L'enregistrement existant n'est pas détenu par cette application."
);
}
var result = submitChange(
action = "DELETE",
record_set = existing.record_set,
comment = "Remove application hostname " & fully_qualified_name
);
result.changed = true;
result.fully_qualified_name = fully_qualified_name;
return result;
}
public struct function getChangeStatus( required string change_id ) {
var request = createObject( "java", "software.amazon.awssdk.services.route53.model.GetChangeRequest" )
.builder()
.id( trim( arguments.change_id ) )
.build();
var response = variables.route53_client.getChange( request );
var change_information = response.changeInfo();
return {
change_id: toString( change_information.id() ),
status: toString( change_information.statusAsString() ),
submitted_at: toString( change_information.submittedAt() )
};
}
public void function close() { variables.route53_client.close(); }
private struct function submitChange(
required string action,
required any record_set,
required string comment
) {
var change_action = createObject(
"java",
"software.amazon.awssdk.services.route53.model.ChangeAction"
).fromValue( uCase( arguments.action ) );
var change = createObject( "java", "software.amazon.awssdk.services.route53.model.Change" )
.builder()
.action( change_action )
.resourceRecordSet( arguments.record_set )
.build();
var change_batch = createObject( "java", "software.amazon.awssdk.services.route53.model.ChangeBatch" )
.builder()
.comment( arguments.comment )
.changes( [ change ] )
.build();
var request = createObject( "java", "software.amazon.awssdk.services.route53.model.ChangeResourceRecordSetsRequest" )
.builder()
.hostedZoneId( variables.hosted_zone_id )
.changeBatch( change_batch )
.build();
var response = variables.route53_client.changeResourceRecordSets( request );
var change_information = response.changeInfo();
return {
change_id: toString( change_information.id() ),
status: toString( change_information.statusAsString() ),
submitted_at: toString( change_information.submittedAt() )
};
}
private struct function findCanonicalNameRecord( required string fully_qualified_name ) {
var record_type = createObject( "java", "software.amazon.awssdk.services.route53.model.RRType" ).CNAME;
var request = createObject( "java", "software.amazon.awssdk.services.route53.model.ListResourceRecordSetsRequest" )
.builder()
.hostedZoneId( variables.hosted_zone_id )
.startRecordName( arguments.fully_qualified_name )
.startRecordType( record_type )
.maxItems( "1" )
.build();
var response = variables.route53_client.listResourceRecordSets( request );
var record_sets = response.resourceRecordSets();
if ( record_sets.isEmpty() ) {
return { found: false };
}
var candidate = record_sets.get( 0 );
if (
normalizeName( candidate.name() ) != normalizeName( arguments.fully_qualified_name )
|| candidate.typeAsString() != "CNAME"
) {
return { found: false };
}
return {
found: true,
record_set: candidate
};
}
private string function buildManagedName( required string label ) {
var selected_label = lCase( trim( arguments.label ) );
if (
len( selected_label ) > 63
|| !reFind( "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", selected_label )
) {
throw(
type = "Route53RecordService.InvalidLabel",
message = "L'étiquette d'hôte est invalide."
);
}
return ensureTrailingDot( selected_label & "." & variables.managed_suffix );
}
private string function normalizeName( required string value ) {
return lCase(
reReplace(
trim( arguments.value ),
"\.$",
"",
"one"
)
);
}
private string function ensureTrailingDot( required string value ) {
var selected_value = trim( arguments.value );
if ( right( selected_value, 1 ) != "." ) {
selected_value &= ".";
}
return selected_value;
}
}Les méthodes publiques acceptent un seul libellé d'hôte. Le code appelant peut demander blue-team, mais il ne peut pas demander mail.example.com, modifier un enregistrement de serveur de noms ou pointer le nom d'hôte vers un site de référencement de cryptomonnaie. La cible du nom canonique provient de la configuration protégée de l'application. L'identifiant de zone hébergée et le suffixe géré aussi.
Le service utilise UPSERT, qui crée l'enregistrement s'il n'existe pas et le met à jour s'il existe. Cela rend le provisionnement sûr à relancer, mais cela signifie aussi que l'application peut remplacer un enregistrement de nom canonique existant dans son suffixe géré. Utilisez un suffixe dédié dont les enregistrements appartiennent exclusivement à l'application.
Amazon décrit le comportement exact de CREATE, DELETE et UPSERT dans la référence ChangeResourceRecordSets.
Création du client global Route 53
Route 53 est un service mondial. Quand nous créons son client Java, nous devons utiliser la région mondiale spéciale du kit de développement logiciel :
<cfscript>
route53_client = application.aws_client_factory.createClient(
service_name = "route53",
region_name = "aws-global"
);
application.route53_record_service = new models.route53_record_service(
route53_client = route53_client,
hosted_zone_id = application.configuration.route53_hosted_zone_id,
managed_suffix = "sites.example.com",
canonical_name_target = "application.example.net",
time_to_live_seconds = 300
);
</cfscript>La région mondiale est la seule correction que nous devons apporter à l'utilisation générique de l'usine de la première partie. La documentation de migration vers la version 2 de Java d'Amazon précise Region.AWS_GLOBAL pour les services sans point de terminaison régional. Notre usine crée des régions par nom, donc aws-global fournit la valeur correspondante.
Fermez le client lorsque l'application se termine :
public void function onApplicationEnd( required struct applicationScope ) {
if ( structKeyExists( arguments.applicationScope, "route53_record_service" ) ) {
arguments.applicationScope.route53_record_service.close();
}
}Comme pour les autres clients de service, créez-le une fois et réutilisez-le. Construire des clients à répétition n'améliorera pas les enregistrements de domaine. Cela fera seulement tourner l'application davantage sans rien accomplir, un modèle que je connais personnellement.
Création ou mise à jour d'un nom d'hôte
Le provisionnement d'un nom d'hôte nécessite un seul appel de méthode :
<cfscript>
result = application.route53_record_service.upsertHostname( label = "blue-team" );
writeDump( result );
</cfscript>Le résultat ressemblera à ceci :
{
"changed": true,
"fully_qualified_name": "blue-team.sites.example.com.",
"change_id": "/change/C0123456789EXAMPLE",
"status": "PENDING",
"submitted_at": "2026-09-15T14:32:17Z"
}PENDING ne veut pas dire que la demande a échoué. Cela veut dire que Route 53 a accepté la modification et la propage aux serveurs de noms faisant autorité. Conservez l'identifiant de changement avec l'enregistrement de provisionnement. Une tâche planifiée ou un travail en arrière-plan peut le vérifier plus tard :
<cfscript>
change = application.route53_record_service.getChangeStatus( change_id = pending_domain_change.change_id );
if ( change.status == "INSYNC" ) {
domain_repository.markActive(
domain_id = pending_domain_change.domain_id
);
}
</cfscript>Ne gardez pas une requête publique ouverte tout en demandant à répétition à Route 53 si elle a terminé. Retournez un état en attente et laissez un processus en arrière-plan poursuivre le travail. Garder une requête ColdFusion ouverte ne rendra pas le DNS plus motivé.
Selon la documentation GetChange d'Amazon, INSYNC signifie que la modification a atteint tous les serveurs de noms faisant autorité de Route 53 pour la zone hébergée. Cela ne veut pas dire que chaque résolveur récursif sur Internet a oublié la réponse précédente. Les enregistrements mis en cache restent en cache selon leur valeur de durée de vie. Le DNS se souvient de nos erreurs exactement aussi longtemps que nous le lui avons demandé.
Suppression d'un nom d'hôte
Supprimer un enregistrement est légèrement moins évident que d'en créer un. Route 53 exige qu'une demande de suppression contienne les mêmes valeurs d'enregistrement que celles utilisées par l'enregistrement existant. Nous ne devrions pas reconstruire l'enregistrement à partir d'hypothèses. Le service récupère l'ensemble d'enregistrements réel et soumet cet objet exact dans la demande de suppression.
<cfscript>
result = application.route53_record_service.deleteHostname( label = "blue-team" );
writeDump( result );
</cfscript>Avant de supprimer, le service vérifie que la cible actuelle correspond toujours à la cible configurée de l'application. Si quelqu'un a modifié l'enregistrement manuellement, la méthode lance Route53RecordService.RecordConflict au lieu de supprimer quelque chose qu'elle ne possède peut-être plus. Cette vérification est intentionnellement suspecte. La suspicion est une qualité sous-estimée dans le code d'infrastructure.
Route 53 traite un lot de modifications de façon transactionnelle - soit chaque modification valide du lot est appliquée, soit aucune ne l'est. Notre exemple soumet une modification à la fois, mais la même structure peut remplacer plusieurs enregistrements connexes en toute sécurité.
Gestion des échecs
Le composant permet aux exceptions AWS de remonter au service appelant. Le code de production devrait les intercepter à la frontière du workflow, consigner l'identifiant de la requête lorsqu'il est disponible, et laisser l'opération de domaine en attente ou en échec. Ne signalez pas qu'un domaine est actif tant que GetChange n'a pas retourné INSYNC. Ne supprimez pas l'enregistrement de base de données de l'application avant que Route 53 accepte la suppression. Ne laissez pas un échec devenir silencieusement un succès simplement parce que l'interface avait l'air plus satisfaite ainsi.
À ce stade, la même application ColdFusion peut :
- Autoriser les téléversements directs depuis le navigateur vers AWS S3
- Modérer les images téléversées avec Amazon Rekognition
- Créer et supprimer des enregistrements de domaine appartenant à l'application avec Route 53
ColdFusion n'avait besoin d'aucune extension propre à un service pour faire tout cela. Il chargeait les bibliothèques Java, créait des objets de requête typés et appelait les mêmes interfaces qu'une application Java peut utiliser. La partie difficile n'était pas d'appeler Amazon. La partie difficile était de décider exactement ce que l'application devrait être autorisée à faire.
C'est généralement la partie difficile. L'appel de méthode ne fait que donner à nos erreurs un endroit coûteux où se produire.