{"id":1463,"date":"2020-06-26T00:14:36","date_gmt":"2020-06-25T22:14:36","guid":{"rendered":"https:\/\/blog.besharp.it\/?p=1463"},"modified":"2021-03-17T15:20:05","modified_gmt":"2021-03-17T14:20:05","slug":"hostare-un-sito-statico-su-aws-cloudfront-e-sempre-la-scelta-giusta","status":"publish","type":"post","link":"https:\/\/blog.besharp.it\/it\/hostare-un-sito-statico-su-aws-cloudfront-e-sempre-la-scelta-giusta\/","title":{"rendered":"Hostare un sito statico su AWS: CloudFront \u00e8 sempre la scelta giusta?"},"content":{"rendered":"
Il binomio di servizi CloudFront<\/strong> – S3<\/strong> fa oramai parte delle pratiche consolidate di molte aziende che hanno la necessit\u00e0 di ospitare un sito statico (o parte di esso) a prezzi contenuti senza rinunciare per\u00f2 ai crismi di sicurezza<\/strong> (come ad esempio la terminazione SSL sul proprio dominio). Ma \u00e8 vero che \u00e8 l\u2019unica via percorribile? Esistono altri modi per conseguire il medesimo risultato che per\u00f2 rispondono ad esigenze differenti? In questo articolo si prova a dare una risposta!<\/span><\/p>\n Immaginiamo di dover rispondere alla seguente esigenza: il team di frontend della propria azienda richiede di poter testare ci\u00f2 che ha appena sviluppato su un\u2019infrastruttura simile a quella finale per mostrare anche solo a livello visivo che il risultato sia quello aspettato. In particolare, avrebbe piacere nell\u2019avere a disposizione un nome di dominio differente per ogni feature in modo tale da non introdurre livelli di inconsistenza relativi ai path.<\/span><\/p>\n Anche senza prendere la calcolatrice in mano per fare i conti tentando di prevedere il traffico gestito, \u00e8 subito chiaro che mantenere una differente distribuzione CloudFront per ogni feature e per ogni progetto frontend possa generare inutili complicazioni a livello di gestione dell\u2019account AWS stesso. Vale la pena far notare che non \u00e8 possibile creare due origin differenziate dalla path principale e collegarle a due domini differenti all\u2019interno della stessa distribuzione CloudFront senza incorrere in almeno un redirect che porterebbe ai problemi di path sopra citati).<\/span><\/p>\n In prima istanza constatiamo la natura effimera di questi deploy. Ci\u00f2 richiede che vi siano dei tempi relativamente ristretti di creazione e rimozione di questi elementi infrastrutturali. Per raggiungere questo obiettivo \u00e8 utile quindi la condivisione delle medesime risorse da parte di pi\u00f9 interlocutori in modo tale da minimizzare l\u2019overhead introdotto.<\/span><\/p>\n La soluzione proposta \u00e8 composta dai seguenti elementi infrastrutturali:<\/span><\/p>\n <\/p>\n Per ottenere il risultato desiderato \u00e8 fondamentale la creazione del Bucket S3 con la configurazione corretta. Di seguito la definizione in CloudFormation:<\/span><\/p>\n Come si pu\u00f2 notare, \u00e8 stata attivata la \u201cwebsite configuration\u201d in modo tale da poterci interfacciare con il bucket tramite chiamate HTTP ma allo stesso tempo \u00e8 presente anche una Bucket Policy che vieta il recupero di un qualsiasi oggetto a meno che la richiesta non passi dal VPC Endpoint di S3, garantendo quindi che solo gli interlocutori che passano dalla VPC dell\u2019account possano accedere al Bucket stesso.<\/p>\n Per permettere all\u2019utente finale di visualizzare il sito statico ospitato su S3 occorre creare un Load Balancer (a seconda di chi possa essere questo utente finale, si pu\u00f2 scegliere se rendere il Load Balancer privato o meno). In CloudFormation questo \u00e8 l\u2019insieme di risorse da creare:<\/p>\n Tramite questo template, viene deployato un Load Balancer pubblico con un listener che ascolta sulla porta 80 (HTTP) che effettua una redirect su 443 (HTTPS) su cui \u00e8 presente un altro listener che per\u00f2 contatta un Target Group su cui \u00e8 registrata una Lambda.<\/p>\n La parte di logica della soluzione \u00e8 demandata alla Lambda. Qui di seguito trovate un esempio di codice a titolo dimostrativo (a tal ragione si consiglia di rivederlo e adattarlo per soluzioni production-ready):<\/p>\n Nonostante non sia di immediata lettura, le operazioni effettuate sono molto semplici: partendo dal DNS name con cui l\u2019utente ha raggiunto il Load Balancer, la Lambda gira la chiamata verso il Bucket S3 costruendo la sottocartella da contattare contenente una determinata feature. Per far si che tutto ci\u00f2 funzioni bisogna chiaramente creare un DNS name per ciascuna feature.<\/p>\n Abbiamo visto come, grazie ad una opportuna configurazione delle risorse coinvolte, sia possibile mantenere un Bucket S3 privato nonostante il website hosting attivo, con lo scopo di avere una versione di un sito statico per ogni sottocartella definita, raggiungibile tramite differenti DNS name. Tale approccio \u00e8 indubbiamente pi\u00f9 veloce e agile quando le necessit\u00e0 sono legate alla verifica dell\u2019aspetto visivo del sito e non della configurazione infrastrutturale, evitando quindi l\u2019onere di tempo e di gestione di una distribuzione CloudFront.<\/p>\nProblema<\/span><\/h2>\n
Soluzione<\/span><\/h2>\n
\n
Creazione e configurazione del Bucket S3<\/span><\/h2>\n
\r\n S3Bucket:\r\n Type: AWS::S3::Bucket\r\n Properties:\r\n BucketName: 'subdomain.mydomain.com'\r\n CorsConfiguration:\r\n CorsRules:\r\n - AllowedHeaders:\r\n - '*'\r\n AllowedMethods:\r\n - GET\r\n - HEAD\r\n - POST\r\n - PUT\r\n - DELETE\r\n AllowedOrigins:\r\n - 'https:\/\/*.mydomain.com'\r\n PublicAccessBlockConfiguration:\r\n BlockPublicAcls: true\r\n BlockPublicPolicy: true\r\n IgnorePublicAcls: true\r\n RestrictPublicBuckets: true\r\n WebsiteConfiguration:\r\n ErrorDocument: error.html\r\n IndexDocument: index.html\r\n \r\n S3BucketPolicy:\r\n Type: AWS::S3::BucketPolicy\r\n Properties:\r\n Bucket: !Ref S3Bucket\r\n PolicyDocument:\r\n Version: '2012-10-17'\r\n Statement:\r\n - Sid: VPCEndpointReadGetObject\r\n Effect: Allow\r\n Principal: \"*\"\r\n Action: s3:GetObject\r\n Resource: !Sub '${S3Bucket.Arn}\/*'\r\n Condition:\r\n StringEquals:\r\n aws:sourceVpce: !Ref S3VPCEndpointId\r\n<\/pre>\n
Creazione e configurazione del Load Balancer<\/h2>\n
LoadBalancer:\r\n Type: AWS::ElasticLoadBalancingV2::LoadBalancer\r\n Properties:\r\n Name: !Sub '${ProjectName}'\r\n LoadBalancerAttributes:\r\n - Key: 'idle_timeout.timeout_seconds'\r\n Value: '60'\r\n - Key: 'routing.http2.enabled'\r\n Value: 'true'\r\n - Key: 'access_logs.s3.enabled'\r\n Value: 'true'\r\n - Key: 'access_logs.s3.prefix'\r\n Value: loadbalancers\r\n - Key: 'access_logs.s3.bucket'\r\n Value: !Ref S3LogsBucketName\r\n Scheme: internet-facing\r\n SecurityGroups:\r\n - !Ref LoadBalancerSecurityGroup\r\n Subnets:\r\n - !Ref SubnetPublicAId\r\n - !Ref SubnetPublicBId\r\n - !Ref SubnetPublicCId\r\n Type: application\r\n \r\n LoadBalancerSecurityGroup:\r\n Type: AWS::EC2::SecurityGroup\r\n Properties:\r\n GroupName: !Sub '${ProjectName}-alb'\r\n GroupDescription: !Sub '${ProjectName} Load Balancer Security Group'\r\n SecurityGroupIngress:\r\n - CidrIp: 0.0.0.0\/0\r\n Description: ALB Ingress rule from world\r\n FromPort: 80\r\n ToPort: 80\r\n IpProtocol: tcp\r\n - CidrIp: 0.0.0.0\/0\r\n Description: ALB Ingress rule from world\r\n FromPort: 443\r\n ToPort: 443\r\n IpProtocol: tcp\r\n Tags:\r\n - Key: Name\r\n Value: !Sub '${ProjectName}-alb'\r\n - Key: Environment\r\n Value: !Ref Environment\r\n VpcId: !Ref VPCId\r\n \r\n HttpListener:\r\n Type: AWS::ElasticLoadBalancingV2::Listener\r\n Properties:\r\n DefaultActions:\r\n - RedirectConfig:\r\n Port: '443'\r\n Protocol: HTTPS\r\n StatusCode: 'HTTP_301'\r\n Type: redirect\r\n LoadBalancerArn: !Ref LoadBalancer\r\n Port: 80\r\n Protocol: HTTP\r\n \r\n HttpsListener:\r\n Type: AWS::ElasticLoadBalancingV2::Listener\r\n Properties:\r\n Certificates:\r\n - CertificateArn: !Ref LoadBalancerCertificateArn\r\n DefaultActions:\r\n - Type: forward\r\n TargetGroupArn: !Ref TargetGroup\r\n LoadBalancerArn: !Ref LoadBalancer\r\n Port: 443\r\n Protocol: HTTPS\r\n \r\n TargetGroup:\r\n Type: AWS::ElasticLoadBalancingV2::TargetGroup\r\n Properties:\r\n Name: !Sub '${ProjectName}'\r\n HealthCheckEnabled: false\r\n TargetType: lambda\r\n Targets:\r\n - Id: !GetAtt Lambda.Arn\r\n DependsOn: LambdaPermission\r\n<\/pre>\n
Codice della Lambda di routing<\/h2>\n
import json\r\nfrom boto3 import client as boto3_client\r\nfrom os import environ as os_environ\r\nimport base64\r\nfrom urllib3 import PoolManager\r\n \r\nhttp = PoolManager()\r\ns3 = boto3_client('s3')\r\n \r\ndef handler(event, context):\r\n try:\r\n print(event)\r\n print(context)\r\n \r\n host = event['headers']['host']\r\n print(\"Host:\", host)\r\n \r\n feature = host.split('.')[0]\r\n feature = \"-\".join(feature.split('-')[1:])\r\n print(\"Feature:\", feature)\r\n \r\n path = event['path'] if event['path'] != \"\/\" else \"\/index.html\"\r\n print(\"Path:\", path)\r\n \r\n query_string_parameters = event['queryStringParameters']\r\n query_string_parameters = [f\"{key}={value}\" for key, value in event['queryStringParameters'].items()]\r\n print(\"Query String Parameters:\", query_string_parameters)\r\n \r\n http_method = event[\"httpMethod\"]\r\n url = f\"http:\/\/{os_environ['S3_BUCKET']}.s3-website-eu-west-1.amazonaws.com\/{feature}{path}{'?' if [] != query_string_parameters else ''}{'&'.join(query_string_parameters)}\"\r\n print(url)\r\n \r\n headers = event['headers']\r\n headers.pop(\"host\")\r\n print(\"Headers:\", headers)\r\n \r\n body = event['body']\r\n print(\"Body:\", body)\r\n \r\n r = http.request(http_method, url, headers=headers, body=body)\r\n print(\"Response:\", r)\r\n print(\"Response Data:\", r.data)\r\n \r\n try:\r\n decoded_response = base64.b64encode(r.data).decode('utf-8')\r\n except:\r\n decoded_response = base64.b64encode(r.data)\r\n \r\n print(\"Decoded Response:\", decoded_response)\r\n print(\"Headers Response:\", dict(r.headers))\r\n return {\r\n 'statusCode': 200,\r\n 'body': decoded_response,\r\n \"headers\": dict(r.headers),\r\n \"isBase64Encoded\": True\r\n }\r\n except Exception as e:\r\n print(e)\r\n return {\r\n 'statusCode': 400\r\n }\r\n<\/pre>\n
Conclusione<\/h2>\n