Sådan oprettes en serverløs URL-forkortelse ved hjælp af AWS Lambda og S3

Brug af grafik fra SAP Scenes Pack

I hele dette indlæg bygger vi en serverløs URL-forkortelse ved hjælp af Amazon Web Services (AWS) Lambda og S3. Selvom du ikke har brug for nogen tidligere erfaring med AWS, antager jeg nogen fortrolighed med ES6 JavaScript og Node.js.

Ironisk nok vil de webadresser, der genereres fra vores URL-forkortelse, ofte være længere end de URL-adresser, de omdirigerer til - dette er fordi vi bruger S3-bucket-webstedsadressen. Mot slutningen af ​​indlægget diskuterer jeg, hvordan du kan tilføje et brugerdefineret domæne for at komme omkring denne begrænsning.

Se demoen

Se koden på Github

Det er relativt let at komme i gang med AWS, og alligevel er der bestemt en opfattet kompleksitet. Antallet af tilgængelige tjenester kan være skræmmende at vælge imellem, da mange af dem overlapper hinanden i funktionalitet. Den langsomme og uintuitive AWS Management Console hjælper heller ikke og den teksttunge online dokumentation. Men i hele dette indlæg håber jeg at demonstrere, at den bedste måde at vedtage AWS-tjenester er at bruge en inkrementel tilgang, og du kan komme i gang ved kun at bruge en håndfuld tjenester.

Vi bruger Serverless Framework til at interagere med AWS, og der er ingen grund til at logge ind på AWS Management Console. Serverless Framework giver en abstraktion over AWS og hjælper med at give projektstruktur og fornuftige konfigurationsindstillinger. Hvis du vil lære mere, før vi kommer i gang, skal du læse deres dokumenter.

Arkitektur

Før vi springer ind i en udvikling, lad os først se på AWS-tjenester, vi bruger til at opbygge vores URL-forkortelse.

For at være vært for vores websted bruger vi Amazon S3 fillagringstjeneste. Vi konfigurerer vores S3-spand, der kan betragtes som en øverste mappe, til at tjene et statisk websted. Webstedet vil bestå af statisk indhold og klientside-scripts. Der er ingen muligheder for at udføre serversiden kode (som f.eks. PHP, Ruby eller Java), men det er fint til vores brugssag.

Vi bruger også en lidt kendt funktion i S3, der giver dig mulighed for at konfigurere videresendelse af objekter inde i S3-spande ved blot at tilføje en websteds-omdirigering-placeringsværdi til metadataene til objektet. Hvis du indstiller dette til en URL, bliver browsere omdirigeret via et HTTP 301-svar og placeringshovedet.

URL'en til et S3-objekt er sammensat af S3-bucket-adressen efterfulgt af objektets navn.

http: // [spand-navn] .s3-hjemmeside-eu-vest-1.amazonaws.com / [objekt-navn]

Det følgende er et eksempel på formatet af et S3-spandobjekt til eu-west-1-regionen.

http://serverless-url-shortener.s3-website-eu-west-1.amazonaws.com/6GpLcdl

Dette objektnavn “6GpLcdl” i slutningen af ​​URL'en i eksemplet ovenfor bliver kortkoden for vores forkortede URL'er. Ved hjælp af denne funktionalitet får vi oprindelig URL-omdirigering såvel som lagringsfunktioner. Vi kræver ikke en database for at gemme detaljerne om, hvilken kortkode der peger på hvilken URL som denne information i stedet vil blive gemt med selve objektet.

Vi opretter en Lambda-funktion til at gemme disse S3-objekter med de relevante metadata i vores S3-spand.

Du kan alternativt bruge AWS SDK-klientsiden i browseren til at gemme objekter. Men det er bedre at udpakke denne funktionalitet i en separat tjeneste. Det giver fordelen ved ikke at skulle bekymre sig om at afsløre sikkerhedsoplysninger og kan mere udvides i fremtiden. Vi kortlægger Lambda-funktionen til et slutpunkt på API Gateway, så den er tilgængelig via et API-opkald.

Kom godt i gang

Gå videre til Serverless Framework-dokumenter og kør gennem deres hurtigstartguide. Som en del af installationsprocessen skal du installere AWS CLI og konfigurere dine AWS-legitimationsoplysninger.

Start med at oprette en package.json-fil i roden af ​​projektet.

{
  "name": "serverløs-url-shortener",
  "scripts": {},
  "afhængigheder": {}
}

Vi ved, at vi bliver nødt til at bruge AWS SDK, så gå videre og installer det fra NPM nu ved at indtaste følgende kommando.

npm installer aws-sdk - gem

Opret nu en config.json-fil også ved projektroten. Vi bruger dette til at gemme brugerdefinerede brugerindstillinger i JSON-format.

Tilføj følgende taster med værdier, der passer til din opsætning.

  • BUCKET - det navn, du vil bruge til din S3-spand. Dette bliver en del af den korte URL, hvis du vælger ikke at tilføje et brugerdefineret domæne. Det skal være unikt for den region, du distribuerer til, så du ikke vælger noget for generisk. Men rol ikke, hvis dit valgte spandnavn allerede er i brug, vil du blive advaret gennem Serverless CLI ved installationen.
  • REGION - AWS-regionen, du ønsker at indsætte til. Det er bedst at vælge det område, der er tættest på dine brugere af ydeevneårsager. Hvis du bare følger med i tutorial, bruger jeg eu-west-1.
  • STAGE - scenen, der skal implementeres til. Du har typisk et iscenesættelsesmiljø, der gentager den samme konfiguration som dit produktionsmiljø. Dette giver dig mulighed for at teste softwareudgivelser på en ikke-destruktiv måde. Da dette er en tutorial, vil jeg implementere til dev-fasen.

Din config.json-fil skal ligner den følgende, når den er fuldført.

{
  "BUCKET": "dit-spand-navn",
  "REGION": "eu-west-1",
  "STAGE": "dev",
}

Opret derefter en anden fil i projektroten, serverless.yml. Dette holder vores Serverless Framework-konfiguration formateret på YAML-markeringssprog.

Inde i denne fil starter vi med at definere vores miljø. Bemærk, hvordan vi kan referere til variabler, der er gemt tidligere i config.json.

service: serverløs-url-shortener

udbyder:
  navn: aws
  runtime: nodejs6.10
  scene: $ {fil (config.json): STAGE}
  region: $ {fil (config.json): REGION}
  iamRoleStatements:
    - Effekt: Tillad
      Handling:
        - s3: PutObject
      Ressource: "arn: aws: s3 ::: $ {fil (config.json): BUCKET} / *"

Afsnittet iamRoleStatements henviser til Identity and Access Management, der bruges til at konfigurere Lambda-tilladelser. Her giver vi Lambda skriveadgang til vores S3 spand.

For at gemme objekter har vi brug for tilladelse til at udføre s3: PutObject-handlingen. Andre tilladelser kan tilføjes her, hvis de kræves af dit projekt. Se S3-dokumenter for andre tilgængelige handlinger.

Ressourceværdien er indstillet til S3-spandens Amazon Resource Name, der bruges til unikt at identificere en bestemt AWS-ressource. Formatet for denne identifikator afhænger af den AWS-tjeneste, der henvises til, men generelt har de følgende format.

ARN: partition: service: region: konto-id: ressource

Under udbyderen tilføj vores funktionskonfiguration.

funktioner:
  butik:
    handler: api.handle
    begivenheder:
      - http:
          sti: /
          metode: post
          cors: sandt

Her definerer vi API-konfigurationen og kortlægger vores Lambda til en HTTP POST-begivenhed på API's basis-URL. En behandler med værdien api.handle henviser til en funktion, der kaldes håndtag, der eksporteres fra api.js (vi har ikke brug for js-filtypenavnet, fordi vi tidligere i serverless.yml satte runtime til nodejs6.10).

Lambda er begivenhedsbaseret, så funktioner udføres kun baseret på foruddefinerede triggere. Her har vi defineret en HTTP-begivenhed, men dette kunne også have været en begivenhed, der er udløst af en DynamoDB-tabel eller en SQS-kø.

Dernæst definerer vi i serverless.yml de AWS-ressourcer, der skal instantieres for os ved installation ved hjælp af CloudFormation. Det er værd at nævne, at du ikke nødvendigvis behøver at konfigurere ressourcer på denne måde, du kan også oprette dem ved hjælp af AWS Management Console. Hvis de korrekte adgangstilladelser er på plads, betyder det ikke noget, hvordan ressourcerne oprettes. Men når du definerer de krævede tjenester i serverless.yml definerer du din 'infrastruktur som kode' og får en række fordele ved at gøre det.

”Infrastruktur som kode er fremgangsmåden til at definere computing og netværksinfrastruktur gennem kildekode, der derefter kan behandles ligesom ethvert softwaresystem. Sådan kode kan opbevares i kildekontrol for at muliggøre auditabilitet og reproducerbare bygninger underlagt testpraksis og den fulde disciplin ved kontinuerlig levering. ”
- Martin Fowler

Gå videre og tilføj ressourcekonfigurationen.

ressourcer:
  Ressourcer:
    ServerlessRedirectS3Bucket:
      Type: AWS :: S3 :: Spand
      Ejendomme:
        BucketName: $ {fil (config.json): BUCKET}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
    ServerlessRedirectS3BucketPolicy:
      Type: AWS :: S3 :: BucketPolicy
      Ejendomme:
        Bucket: $ {fil (config.json): BUCKET}
        PolicyDocument:
          Udmelding:
          - Handling:
            - s3: GetObject
            Effekt: Tillad
            ressource:
            - arn: aws: s3 ::: $ {fil (config.json): BUCKET} / *
            Opdragsgiver: "*"

Vi beder om en S3-bucket-ressource, der er konfigureret til at bruge statisk webstedshosting med index.html som roddokument. S3 spande med god grund er private som standard, og derfor er vi nødt til at oprette en S3 spandpolitik, der giver offentlig adgang til den. Uden denne politik ville besøgende i stedet ved at få vist en ikke-godkendt fejlmeddelelse.

Opbygning af API

Vores Lambda-funktion er ansvarlig for fire opgaver.

  1. Grib URL'en for at forkorte fra brugerens formularindgivelse.
  2. Generering af en unik kortkode til URL'en.
  3. Gemning af det relevante omdirigeringsobjekt til S3.
  4. Returnering af objektets vej til klienten.

Opret behandleren

Opret en ny fil kaldet api.js og eksporter en pilefunktion, der kaldes håndtag, der tager tre argumenter: begivenhed, kontekst og tilbagekald. Disse leveres af AWS, når operatøren påberåbes. Denne fil er et Node.js-script, og for at eksportere pilefunktionen skal du tilføje den til module.exports.

module.exports.handle = (begivenhed, kontekst, tilbagekald) => {
}

Denne handler bliver aktiveret, når der fremsættes en HTTP POST-anmodning til vores slutpunkt. For at returnere et API-svar skal vi bruge den medfølgende tilbagekaldsfunktion, der leveres som det tredje pilefunktionsargument. Det er en fejl-første tilbagekald, der tager to argumenter. Hvis forespørgslen er udført med succes, skal null gives som det første argument. Svarobjektet, der er sendt som det andet argument, bestemmer typen af ​​svar, der skal returneres til brugeren. Generering af et svar er så simpelt som at tilvejebringe en statuskode og krop, som det er vist i eksemplet nedenfor.

const response = {
  statusKode: 201,
  body: JSON.stringify ({"shortUrl": "http://example.com"})
}
tilbagekald (null, svar)

Det kontekstobjekt, der blev sendt som det andet argument til behandleren, indeholder runtime-oplysninger, som vi ikke har brug for adgang til til denne tutorial. Vi er dog nødt til at gøre brug af den begivenhed, der blev overført som det første argument, da dette indeholder formularen indsendelse med den URL, der skal forkortes.

Analyser anmodningen

Nedenfor er et eksempel på en API Gateway-begivenhed, der overføres til vores handler, når en bruger indsender en formular. Når vi bygger vores URL-forkortelse som en enkelt sides applikation, sender vi formularen ved hjælp af JavaScript, og indholdstypen bliver derfor applikation / json snarere end applikation / x-www-form-urlencoded.

{
   ressource: '/',
   sti:'/',
   httpMethod: 'POST',
   overskrifter: {
      Acceptere:'*/*',
      'Accept-Encoding': 'gzip, deflate',
      'Cache-kontrol': 'no-cache',
      'CloudFront-Forwarded-Proto': 'https',
      'CloudFront-Is-Desktop-Viewer': 'sand',
      'CloudFront-Is-Mobile-Viewer': 'falsk',
      'CloudFront-Is-SmartTV-Viewer': 'falsk',
      'CloudFront-Is-tablet-Viewer': 'falsk',
      'CloudFront-Viewer-Land': 'GB',
      'Content-type': 'ansøgning / JSON',
      Vært:'',
      'User-Agent': '',
      'X-AMZ-Cf-Id': '',
      'X-AMZN-Trace-Id': '',
      'X-Forwarded-For': '',
      'X-Forwarded-Port': '443',
      'X-Forwarded-Proto': 'https'
   },
   queryStringParameters: null,
   pathParameters: {},
   stageVariables: null,
   requestContext: {
      sti: '/ dev',
      Konto-id:'',
      RESOURCEID: '',
      etape: 'dev',
      requestid: '',
      identitet:{
         cognitoIdentityPoolId: null,
         AccountID: null,
         cognitoIdentityId: null,
         opkalds: null,
         apiKey: '',
         sourceIp: '',
         accesskey: null,
         cognitoAuthenticationType: null,
         cognitoAuthenticationProvider: null,
         userArn: null,
         UserAgent: '',
         bruger: null
      },
      resourcePath: '/',
      httpMethod: 'POST',
      apiId: ''
   },
   krop: '{ "URL": "http://example.com"}',
   isBase64Encoded: false
}

Vi har kun brug af formularen indsendelse fra begivenheden, som vi kan få ved at se på anmodningsorganet. Anmodningsorganet gemmes som et strengt JavaScript-objekt, som vi kan gribe ind i vores handler ved hjælp af JSON.parse (). Ved at drage fordel af JavaScript-kortslutningsevaluering kan vi indstille en standardværdi for en tom streng for tilfælde, hvor en URL ikke er blevet sendt som en del af formularindgivelsen. Dette giver os mulighed for at behandle tilfælde, hvor URL-adressen mangler, og hvor URL-adressen er en tom streng.

module.exports.handle = (begivenhed, kontekst, tilbagekald) => {
  lad longUrl = JSON.parse (event.body) .url || ''
}

Valider URL'en

Lad os tilføje nogle grundlæggende valideringer for at kontrollere, at den angivne URL ser ud til at være legitim. Der er flere tilgange, der kan tages for at opnå dette. Men til formålet med denne tutorial vil vi holde det enkelt og bruge det indbyggede Node.js URL-modul. Vi bygger vores validering for at returnere et løst løfte på en gyldig URL og returnere et afvist løfte på en ugyldig URL. Løfter i JavaScript kan kædes sekventielt, så opløsningen af ​​det ene løfte overgår til succesens næste succes. Vi bruger denne attribut af løfter til at strukturere vores handler. Lad os skrive valideringsfunktionen ved hjælp af løfter.

const url = kræver ('url')
funktion validere (longUrl) {
  if (longUrl === '') {
    returner Promise.reject ({
      statusKode: 400,
      besked: 'URL kræves'
    })
  }
lad parsedUrl = url.parse (longUrl)
  if (parsedUrl.protocol === null || parsedUrl.host === null) {
    returner Promise.reject ({
      statusKode: 400,
      besked: 'URL er ugyldig'
    })
  }
returner Promise.resolve (longUrl)
}

I vores valideringsfunktion kontrollerer vi først, at webadressen ikke er indstillet til en tom streng. Hvis det er, returnerer vi et afvist løfte. Bemærk, hvordan den afviste værdi er et objekt, der indeholder en statuskode og en meddelelse. Vi bruger dette senere til at oprette et passende API-svar. Opkaldsprøve på Node.js url-modulet returnerer et URL-objekt med oplysninger, der kunne udvindes fra den URL, der blev sendt som et strengargumenter. Som en del af vores grundlæggende URL-validering kontrollerer vi blot, om en protokol (for eksempel 'http') og en vært (som 'eksempel.com') kunne udvindes. Hvis en af ​​disse værdier er null på det returnerede URL-objekt, antager vi, at webadressen er ugyldig. Hvis webadressen er gyldig, returnerer vi den som en del af et løst løfte.

Vender tilbage et svar

Efter at have taget URL-adressen fra anmodningen, kalder vi validering, og for hvert yderligere handlertrin, der kræves, returnerer vi et nyt løfte i det foregående løfts succeshåndterer. Den endelige succeshåndterer er ansvarlig for at returnere et API-svar gennem håndtagets callback-argument. Det påberåbes for både fejl-API-svar genereret fra afviste løfter såvel som vellykkede API-svar.

module.exports.handle = (begivenhed, kontekst, tilbagekald) => {
  lad longUrl = JSON.parse (event.body) .url || ''
  validere (longUrl)
    .then (funktion (sti) {
      lad respons = buildResponse (200, 'succes', sti)
      returner Promise.resolve (svar)
    })
    .fang (funktion (fejle) {
      lad respons = buildResponse (err.statusCode, err.message)
      returner Promise.resolve (svar)
    })
    .then (funktion (svar) {
      tilbagekald (null, svar)
    })
}
function buildResponse (statusCode, meddelelse, sti = falsk) {
  lad krop = {besked}
  if (sti) krop ['sti'] = sti
  
  Vend tilbage {
    overskrifter: {
      'Access-Control-Allow-origin': '*'
    },
    statusCode: statusCode,
    body: JSON.stringify (body)
  }
}

Generer en URL-kode

API'en skal være i stand til at generere unikke URL-kortkoder, der er repræsenteret som filnavne i S3-spanden. Da en kort kode kun er et filnavn, er der en stor grad af fleksibilitet i, hvordan den er sammensat. Til vores kortkode bruger vi en 7-cifret alfanumerisk streng, der består af både store og små bogstaver, dette oversættes til 62 mulige kombinationer for hvert tegn. Vi bruger rekursion til at opbygge kortkoden ved at vælge et tegn ad gangen, indtil syv er valgt.

funktion generatorPath (sti = '') {
  lad tegn = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  lad position = Math.floor (Math.random () * tegn.længde)
  lad karakter = characters.charAt (position)
if (sti.længde === 7) {
  retursti
}
return generPath (sti + tegn)
}

Selvom chancen for tilfældigt at generere den samme kortkode er lille (der faktisk er en 0,000000000000000000000000008063365516 chance for at to kortkoder vil være den samme), er vi nødt til at kontrollere, om den genererede kortkode allerede er i brug, hvilket vi kan gøre ved hjælp af AWS SDK. Der er en headObject-metode på S3-tjenesten, der indlæser et objekts metadata. Vi kan bruge dette til at teste, om et objekt med samme navn allerede findes, som når et objekt ikke findes et løfte med koden NotFound afvises. Dette afviste løfte angiver, at kortkoden er gratis og kan bruges. Opkald til headObject er mere performant end at teste, om objektet findes gennem getObject, som indlæser hele objektet.

const AWS = kræver ('aws-sdk')
const S3 = ny AWS.S3 ()
funktion erPathFree (sti) {
  returner S3.headObject (buildRedirect (sti)). løfte ()
    .then (() => Promise.resolve (false))
    .fang (funktion (fejle) {
      if (err.code == 'NotFound') {
        returner Promise.resolve (sand)
      } andet {
        returner Promise.reject (err)
      }
    })
}
funktion buildRedirect (sti, longUrl = falsk) {
  lad omdirigering = {
    'Bucket': config.BUCKET,
    'Nøgle': sti
  }
hvis (longUrl) {
    redirect ['WebsiteRedirectLocation'] = longUrl
  }
vende tilbage
}

Vi kan bruge isPathFree til rekursivt at finde en unik objektsti.

funktion getPath () {
  returner nyt løfte (funktion (løsning, afvisning) {
    lad sti = generere sti ()
    isPathFree (sti)
      .then (funktion (er gratis) {
        retur er gratis? resolut (sti): resolut (getPath ())
      })
  })
}

Udnyttelse af evnen til at kæde løfter vi returnerer en ny påkaldelse af getPath, hvis isPathFree returnerer falsk.

For at gemme et objekt, når der er fundet en unik kortkode, skal vi bare kalde putObject-metoden på AWS SDK S3-tjenesten. Lad os pakke dette op i en funktion, der løser kortkoden, hvis opkaldet til putObject-metoden var vellykket og returnerer et fejlobjekt for at opbygge et API-svar, hvis det ikke gjorde det.

funktion saveRedirect (omdirigering) {
  returner S3.putObject (omdirigering) .promise ()
    .then (() => Promise.resolve (omdirigér ['Key']))
    .catch (() => Promise.reject ({
      statusKode: 500,
      besked: 'Fejl ved gemning af omdirigering'
  })
}

Ved at bruge de ovennævnte funktioner kan vi tilføje to nye løftesucceshåndterere til at færdiggøre vores API-endpoint. Vi er nødt til at returnere getPath fra den første løftesucceshåndterer, der løser en unik URL-kortkode. Returnering af saveRedirect med et omdirigeringsobjekt bygget ved hjælp af denne unikke kortkode i den anden succeshåndtering gemmer objektet i S3-spanden. Dette objekts sti kan derefter returneres til klienten som en del af et API-svar. Vores handler skal nu være komplet.

module.exports.handle = (begivenhed, kontekst, tilbagekald) => {
  lad longUrl = JSON.parse (event.body) .url || ''
  validere (longUrl)
    .then (funktion () {
      return getPath ()
    })
    .then (funktion (sti) {
      lad omdirigering = buildRedirect (sti, longUrl)
      return saveRedirect (omdirigering)
    })
    .then (funktion (sti) {
      lad respons = buildResponse (200, 'succes', sti)
      returner Promise.resolve (svar)
    })
    .fang (funktion (fejle) {
      lad respons = buildResponse (err.statusCode, err.message)
      returner Promise.resolve (svar)
    })
    .then (funktion (svar) {
      tilbagekald (null, svar)
    })
}

Deplo API'en

Kør serverløs implementering i din terminal for at distribuere API'et til AWS. Dette indstiller vores S3-spand og returnerer URL-adressen til slutpunktet. Hold URL-adressen til slutpunktet godt, da vi har brug for det senere.

Serverløs: Pakningstjeneste ...
Serverløs: Eksklusiv udviklingsafhængighed ...
Serverløs: Uploader CloudFormation-fil til S3 ...
Serverløs: Upload af artefakter ...
Serverløs: Uploadtjeneste .zip-fil til S3 (5.44 MB) ...
Serverløs: Validerer skabelon ...
Serverløs: Opdaterer Stack ...
Serverløs: Kontrollerer status for opdatering af stack ...
..............
Serverløs: Stakopdatering er afsluttet ...
Serviceinformation
service: serverløs-url-shortener
scene: dev
region: eu-west-1
stack: serverless-url-shortener-dev
api-taster:
  Ingen
effektmål:
  POST - https://t2fgbcl26h.execute-api.eu-west-1.amazonaws.com/dev/
funktioner:
  butik: serverless-url-shortener-dev-store
Serverløs: Fjernelse af gamle serviceversioner ...

Oprettelse af frontend

For at hjælpe med frontend-design bruger vi PaperCSS-rammen. Vi får også fat i jQuery for at forenkle arbejdet med DOM og stille AJAX-forespørgsler. Det er værd at bemærke, at for et produktionsmiljø, du sandsynligvis ønsker at trække i to lettere afhængigheder, men da dette kun er en tutorial, synes jeg, det er acceptabelt.

Opret en statisk mappe, så vi har et sted at gemme vores frontend-kode.

Download afhængighederne

Gem en kopi af paper.min.css og jquery-3.2.1.min.js i vores nyligt oprettede statiske mappe, disse er minificerede versioner af henholdsvis PaperCSS-rammen og jQuery-biblioteket.

Tilføj HTML

Opret en ny fil kaldet index.html i den statiske mappe, og tilføj den krævede HTML. Vi har brug for en formular med en URL-indtastning og en knap for at indsende formularen. Vi har også brug for et sted for at placere resultatet af alle API-opkald, som for et vellykket API-opkald ville være den forkortede URL, og for et mislykket API-opkald ville dette være fejlmeddelelsen.




  
  
   Serverløs url-forkortelse