diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml index e6a47fdb..a51b9ce6 100644 --- a/.github/workflows/build-and-deploy.yaml +++ b/.github/workflows/build-and-deploy.yaml @@ -26,79 +26,105 @@ jobs: build-and-deploy: needs: increase-version - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - - name: "Prepare: Checkout" - uses: actions/checkout@v1 + name: "Infrastructure: Checkout" + uses: actions/checkout@v2 + with: + path: aws + repository: undergroundwires/aws-static-site-with-cd - - name: "Prepare: Create AWS user profile" + name: "Infrastructure: Create AWS user profile & session name" run: >- - bash "aws/scripts/configure/create-user-profile.sh" \ + bash "scripts/configure/create-user-profile.sh" \ --profile user \ --access-key-id ${{secrets.AWS_DEPLOYMENT_USER_ACCESS_KEY_ID}} \ --secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \ - --region us-east-1 + --region us-east-1 \ + && \ + echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" + working-directory: aws - name: "Infrastructure: Deploy IAM stack" run: >- - bash "aws/scripts/deploy/deploy-stack.sh" \ - --template-file aws/iam-stack.yaml \ + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/iam-stack.yaml \ --stack-name privacysexy-iam-stack \ - --capabilities CAPABILITY_IAM \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "WebStackName=privacysexy-web-stack DnsStackName=privacysexy-dns-stack \ + CertificateStackName=privacysexy-cert-stack RootDomainName=privacy.sexy" \ --region us-east-1 --role-arn ${{secrets.AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN}} \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} - - - name: "Infrastructure: Deploy certificate stack" - run: >- - bash "aws/scripts/deploy/deploy-stack.sh" \ - --template-file aws/certificate-stack.yaml \ - --stack-name privacysexy-certificate-stack \ - --region us-east-1 \ - --role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws - name: "Infrastructure: Deploy DNS stack" run: >- - bash "aws/scripts/deploy/deploy-stack.sh" \ - --template-file aws/dns-stack.yaml \ + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/dns-stack.yaml \ --stack-name privacysexy-dns-stack \ + --parameter-overrides "RootDomainName=privacy.sexy" \ --region us-east-1 \ --role-arn ${{secrets.AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN}} \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "Infrastructure: Deploy certificate stack" + run: >- + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/certificate-stack.yaml \ + --stack-name privacysexy-cert-stack \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "IamStackName=privacysexy-iam-stack RootDomainName=privacy.sexy DnsStackName=privacysexy-dns-stack" \ + --region us-east-1 \ + --role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \ + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws - name: "Infrastructure: Deploy web stack" run: >- - bash "aws/scripts/deploy/deploy-stack.sh" \ - --template-file aws/web-stack.yaml \ + bash "scripts/deploy/deploy-stack.sh" \ + --template-file stacks/web-stack.yaml \ --stack-name privacysexy-web-stack \ + --parameter-overrides "CertificateStackName=privacysexy-cert-stack DnsStackName=privacysexy-dns-stack \ + RootDomainName=privacy.sexy UseDeepLinks=true" \ + --capabilities CAPABILITY_IAM \ --region us-east-1 \ --role-arn ${{secrets.AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN}} \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} + --profile user --session ${{ env.SESSION_NAME }} + working-directory: aws + - + name: "App: Checkout" + uses: actions/checkout@v2 + with: + path: site - name: "App: Setup node" uses: actions/setup-node@v1 with: - node-version: '11.x' + node-version: '12.x' - name: "App: Install dependencies" run: npm install + working-directory: site - name: "App: Run tests" run: npm run test:unit + working-directory: site - name: "App: Build" run: npm run build + working-directory: site - name: "App: Deploy to S3" run: >- bash "aws/scripts/deploy/deploy-to-s3.sh" \ - --folder dist \ + --folder site/dist \ --web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \ --storage-class ONEZONE_IA \ --role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \ --region us-east-1 \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} + --profile user --session ${{ env.SESSION_NAME }} - name: "App: Invalidate CloudFront cache" run: >- @@ -107,4 +133,4 @@ jobs: --web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \ --role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \ --region us-east-1 \ - --profile user --session ${{github.actor}}-${{github.event_name}}-${{github.sha}} \ No newline at end of file + --profile user --session ${{ env.SESSION_NAME }} \ No newline at end of file diff --git a/README.md b/README.md index 5dcd4ae5..9502507e 100644 --- a/README.md +++ b/README.md @@ -56,64 +56,18 @@ Fork it & add more scripts in [application.yaml](src/application/application.yam ### AWS Infrastructure -- The application runs in AWS 100% serverless and automatically provisioned using [CloudFormation files](/aws) and GitHub Actions. -- Maximum security & automation and minimum AWS costs were the highest priorities of the design. +[![AWS solution](docs/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd) -![AWS solution](docs/aws-solution.png) +- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) + - Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/). + - Maximum security & automation and minimum AWS costs are the highest priorities of the design. #### GitOps: CI/CD to AWS - Everything that's merged in the master goes directly to production. - - Deploy infrastructure ► Deploy web application ► Invalidate CloudFront Cache -- See more at [build-and-deploy.yaml](.GitHub/workflows/build-and-deploy.yaml) +- See more at [build-and-deploy.yaml](.github/workflows/build-and-deploy.yaml), and [run-tests.yaml](.github/workflows/run-tests.yaml) -![CI/CD to AWS with GitHub Actions](docs/gitops.png) - -##### CloudFormation - -![CloudFormation design](docs/aws-cloudformation.png) - -- AWS infrastructure is defined as code with following files: - - `iam-stack`: Creates & updates the deployment user. - - Everything in IAM layer is fine-grained using least privileges principle. - - Each deployment step has its own temporary credentials with own permissions. - - `certificate-stack.yaml` - - It'll generate SSL certification for the root domain and www subdomain. - - ❗ It [must](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-invalid-viewer-certificate/) be deployed in `us-east-1` to be able to be used by CloudFront by `web-stack`. - - It uses CustomResource and a lambda instead of native `AWS::CertificateManager::Certificate` because: - - Problem: - - AWS variant waits until a certificate is validated. - - There's no way to automate validation without workaround. - - Solution: - - Deploy a lambda that deploys the certificate (so we don't wait until certificate is validated) - - Get DNS records to be used in validation & export it to be used later. - - `web-stack.yaml`: It'll deploy S3 bucket and CloudFront in front of it. - - `dns-stack.yaml`: It'll deploy Route53 hosted zone - - Each time Route53 hosted zone is re-created it's required to update the DNS records in the domain registrar. See *Configure your domain registrar*. -- I use cross stacks instead of single stack or nested stacks because: - - Easier to test & maintain & smaller files and different lifecycles for different areas. - - It allows to deploy web bucket in different region than others as other stacks are global (`us-east-1`) resources. - -##### Initial deployment - -- ❗ Prerequisite: A registered domain name for website. - -1. **Configure build agent (GitHub actions)** - - Deploy manually `iam-stack.yaml` with stack name `privacysexy-iam-stack` (to follow the convention) - - It'll give you deploy user. Go to console & generate secret id + key (Security credentials => Create access key) for the user [IAM users](https://console.aws.amazon.com/iam/home#/users). - - 🚶 Deploy secrets: - - Add secret id & key in GitHub Secrets. - - `AWS_DEPLOYMENT_USER_ACCESS_KEY_ID`, `AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY` - - Add more secrets given from Outputs section of the CloudFormation stack. - - Run GitHub actions to deploy rest of the application. - - It'll run `certificate-stack.yaml` and then `iam-stack.yaml`. - -2. **Configure your domain registrar** - - ❗ **Web stack will fail** after DNS stack because you need to validate your domain. - - 🚶 Go to your domain registrar and change name servers to NS values - - `dns-stack.yaml` outputs those in CloudFormation stack. - - You can alternatively find those in [Route53](https://console.aws.amazon.com/route53/home#hosted-zones) - - When nameservers of your domain updated, the certification will get validated automatically, you can then delete the failed stack in CloudFormation & re-run the GitHub actions. +[![CI/CD to AWS with GitHub Actions](docs/gitops.png)](.github/workflows/build-and-deploy.yaml) ## Thank you for the awesome projects 🍺 diff --git a/aws/certificate-stack.yaml b/aws/certificate-stack.yaml deleted file mode 100644 index 7a75a035..00000000 --- a/aws/certificate-stack.yaml +++ /dev/null @@ -1,211 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Creates certificate for the root + www subdomain. !! It must be deployed in us-east-1 to be able to be used by CloudFront. - -Parameters: - - RootDomainName: - Type: String - Default: privacy.sexy - Description: The root DNS name of the website e.g. privacy.sexy - AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(? - 'use strict'; - const aws = require('aws-sdk'); - const acm = new aws.ACM(); - const log = (t) => console.log(t); - - exports.handler = async (event, context) => { - log(`Request recieved:\n${JSON.stringify(event)}`); - const userData = event.ResourceProperties; - const rootDomain = userData.RootDomainName; - let data = null; - try { - switch(event.RequestType) { - case 'Create': - data = await handleCreateAsync(rootDomain, userData.Tags); - break; - case 'Update': - data = await handleUpdateAsync(); - break; - case 'Delete': - data = await handleDeleteAsync(rootDomain); - break; - } - await sendResponseAsync(event, context, 'SUCCESS', data); - } catch(error) { - await sendResponseAsync(event, context, 'ERROR', { - title: `Failed to ${event.RequestType}, see error`, - error: error - }); - } - } - - async function handleCreateAsync(rootDomain, tags) { - const { CertificateArn } = await acm.requestCertificate({ - DomainName: rootDomain, - SubjectAlternativeNames: [`www.${rootDomain}`], - Tags: tags, - ValidationMethod: 'DNS', - }).promise(); - log(`Cert requested:${CertificateArn}`); - const waitAsync = (ms) => new Promise(resolve => setTimeout(resolve, ms)); - const maxAttempts = 10; - let options = undefined; - for (let attempt = 0; attempt < maxAttempts && !options; attempt++) { - await waitAsync(2000); - const { Certificate } = await acm.describeCertificate({ CertificateArn }).promise(); - if(Certificate.DomainValidationOptions.filter((o) => o.ResourceRecord).length === 2) { - options = Certificate.DomainValidationOptions; - } - } - if(!options) { - throw new Error(`No records after ${maxAttempts} attempts.`); - } - return getResponseData(options, CertificateArn, rootDomain); - } - - async function handleDeleteAsync(rootDomain) { - const certs = await acm.listCertificates({}).promise(); - const cert = certs.CertificateSummaryList.find((cert) => cert.DomainName === rootDomain); - if (cert) { - await acm.deleteCertificate({ CertificateArn: cert.CertificateArn }).promise(); - log(`Deleted ${cert.CertificateArn}`); - } else { - log('Cannot find'); // Do not fail, delete can be called when e.g. CF fails before creating cert - } - return null; - } - - async function handleUpdateAsync() { - throw new Error(`Not yet implemented update`); - } - - function getResponseData(options, arn, rootDomain) { - const findRecord = (url) => options.find(option => option.DomainName === url).ResourceRecord; - const root = findRecord(rootDomain); - const www = findRecord(`www.${rootDomain}`); - const data = { - CertificateArn: arn, - RootVerificationRecordName: root.Name, - RootVerificationRecordValue: root.Value, - WwwVerificationRecordName: www.Name, - WwwVerificationRecordValue: www.Value, - }; - return data; - } - - /* cfn-response can't async / await :( */ - async function sendResponseAsync(event, context, responseStatus, responseData, physicalResourceId) { - return new Promise((s, f) => { - var b = JSON.stringify({ - Status: responseStatus, - Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`, - PhysicalResourceId: physicalResourceId || context.logStreamName, - StackId: event.StackId, - RequestId: event.RequestId, - LogicalResourceId: event.LogicalResourceId, - Data: responseData - }); - log(`Response body:\n${b}`); - var u = require("url").parse(event.ResponseURL); - var r = require("https").request( - { - hostname: u.hostname, - port: 443, - path: u.path, - method: "PUT", - headers: { - "content-type": "", - "content-length": b.length - } - }, (p) => { - log(`Status code: ${p.statusCode}`); - log(`Status message: ${p.statusMessage}`); - s(context.done()); - }); - r.on("error", (e) => { - log(`request failed: ${e}`); - f(context.done(e)); - }); - r.write(b); - r.end(); - }); - } - -Outputs: - CertificateArn: - Description: The Amazon Resource Name (ARN) of an AWS Certificate Manager (ACM) certificate. - Value: !GetAtt AcmCertificateForHostedZone.CertificateArn - Export: - Name: !Join [':', [ !Ref 'AWS::StackName', CertificateArn ]] - - RootVerificationRecordName: - Description: Name for root domain CNAME verification record - Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordName - Export: - Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordName ]] - - RootVerificationRecordValue: - Description: Value for root domain name CNAME verification record - Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordValue - Export: - Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordValue ]] - - WwwVerificationRecordName: - Description: Name for www domain name CNAME verification record - Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordName - Export: - Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordName ]] - - WwwVerificationRecordValue: - Description: Value for www domain name CNAME verification record - Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordValue - Export: - Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordValue ]] diff --git a/aws/dns-stack.yaml b/aws/dns-stack.yaml deleted file mode 100644 index 59eb983a..00000000 --- a/aws/dns-stack.yaml +++ /dev/null @@ -1,61 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Creates hosted zone & sets up records for the CloudFront URL. - -Parameters: - - RootDomainName: - Type: String - Default: privacy.sexy - Description: The root DNS name of the website e.g. privacy.sexy - AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(? Deploys the identity management for the deployment - -# Granulatiy cheatsheet: https://iam.cloudonaut.io/ - -Parameters: - WebStackName: - Type: String - Default: privacysexy-web-stack - Description: Name of the web stack. - DnsStackName: - Type: String - Default: privacysexy-dns-stack - Description: Name of the DNS stack. - CertificateStackName: - Type: String - Default: privacysexy-certificate-stack - Description: Name of the IAM stack. - -Resources: - - # ----------------------------- - # ------ User & Group --------- - # ----------------------------- - DeploymentGroup: - Type: AWS::IAM::Group - Properties: - # GroupName: No hardcoded naming because of easier CloudFormation management - ManagedPolicyArns: - - !Ref AllowValidateTemplatePolicy - - DeploymentUser: - Type: AWS::IAM::User - Properties: - # # UserName: No hardcoded naming because of easier CloudFormation management - # # Policies: Assing policies on group level - Tags: - - - Key: Application - Value: privacy.sexy - - AddDeploymentUserToDeploymentGroup: - Type: AWS::IAM::UserToGroupAddition - Properties: - GroupName: !Ref DeploymentGroup - Users: - - !Ref DeploymentUser - - # ----------------------------- - # ----------- Roles ----------- - # ----------------------------- - IamStackDeployRole: - Type: AWS::IAM::Role - Properties: - Description: Allows to deploy IAM stack - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref CloudFormationDeployPolicy - - !Ref PolicyDeployPolicy - - !Ref IamStackDeployPolicy - - CertificateStackDeployRole: - Type: AWS::IAM::Role - Properties: - Description: Allows to deploy certificate stack - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref CloudFormationDeployPolicy - - !Ref LambdaBackedCustomResourceDeployPolicy - - DnsStackDeployRole: - Type: AWS::IAM::Role - Properties: - Description: Allows to deploy DNS stack - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref CloudFormationDeployPolicy - - !Ref DnsStackDeployPolicy - - WebStackDeployRole: - Type: AWS::IAM::Role - Properties: - Description: Allows to deploy web stack - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref CloudFormationDeployPolicy - - !Ref WebStackDeployPolicy - - S3SiteDeployRole: - Type: 'AWS::IAM::Role' - Properties: - Description: "Allows to deploy website to S3" - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref S3SiteDeployPolicy - - !Ref StackExportReaderPolicy - - CloudFrontSiteDeployRole: - Type: 'AWS::IAM::Role' - Properties: - Description: "Allows to informs to CloudFront to renew its cache from S3" - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - AWS: !GetAtt DeploymentUser.Arn - Action: sts:AssumeRole - Tags: - - - Key: Application - Value: privacy.sexy - ManagedPolicyArns: - - !Ref CloudFrontInvalidationPolicy - - !Ref StackExportReaderPolicy - - ResolveCertificateLambdaRole: # See certificate stack - Type: AWS::IAM::Role - Properties: - Description: Allow deployment of certificates - AssumeRolePolicyDocument: - Statement: - - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - !Ref CertificateDeployPolicy - - # -------------------------------- - # ----------- Policies ----------- - # -------------------------------- - - AllowValidateTemplatePolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: "No read & writes to resources, reveals just basic CloudFormation API to be used for validating templates" - # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowCloudFormationTemplateValidation - Effect: Allow - Action: - - cloudformation:ValidateTemplate - Resource: '*' - - CloudFormationDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: "Allows deploying CloudFormation using CLI command 'aws cloudformation deploy' (with change sets)" - # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowCloudFormationStackOperations - Effect: Allow - Action: - - cloudformation:GetTemplateSummary - - cloudformation:DescribeStacks - - cloudformation:CreateChangeSet - - cloudformation:ExecuteChangeSet - - cloudformation:DescribeChangeSet - Resource: - - !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${WebStackName}/* - - !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${DnsStackName}/* - - !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${AWS::StackName}/* - - !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${CertificateStackName}/* - - IamStackDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deploying IAM CloudFormation stack. - # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowUserArnExport - Effect: Allow - Action: - - iam:GetUser - Resource: - - !GetAtt DeploymentUser.Arn - - - Sid: AllowTagging - Effect: Allow - Action: - - iam:TagResource - Resource: - - !Sub arn:aws:cloudformation::${AWS::AccountId}:stack/${AWS::StackName}/* - - !GetAtt DeploymentUser.Arn - - - Sid: AllowRoleDeployment - Effect: Allow - Action: - - iam:CreateRole - Resource: - - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-* - - LambdaBackedCustomResourceDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deploying a lambda-backed custom resource. - # ManagedPolicyName: # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowLambdaDeployment - Effect: Allow - Action: - - lambda:GetFunction - - lambda:DeleteFunction - - lambda:CreateFunction - - lambda:GetFunctionConfiguration - - lambda:InvokeFunction - Resource: - - !Sub arn:aws:lambda:*:${AWS::AccountId}:function:${CertificateStackName}* - - - Sid: AllowPassingLambdaRole - Effect: Allow - Action: - - iam:PassRole - Resource: - - !GetAtt ResolveCertificateLambdaRole.Arn - - CertificateDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deploying certifications stack. - # ManagedPolicyName: # ManagedPolicyName: No hardcoded naming because of easier CloudFormation management - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowCertificateDeployment - Effect: Allow - Action: - - acm:RequestCertificate - - acm:DescribeCertificate - - acm:DeleteCertificate - - acm:AddTagsToCertificate - - acm:ListCertificates - Resource: '*' # Certificate Manager does not support resource level IAM - - PolicyDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deployment of policies - # ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowPolicyUpdates - Effect: Allow - Action: - - iam:ListPolicyVersions - - iam:CreatePolicyVersion - - iam:DeletePolicyVersion - - iam:CreatePolicy - - iam:DeletePolicy - - iam:GetPolicy - Resource: - - !Sub arn:aws:iam::${AWS::AccountId}:policy/${AWS::StackName}-* # when ManagedPolicyName is not given policies get name like StackName-* - - - Sid: AllowPoliciesOnRoles - Effect: Allow - Action: - - iam:AttachRolePolicy - - iam:DetachRolePolicy - - iam:GetRole - Resource: - - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-* - - - Sid: AllowPolicyAssigmentToGroup - Effect: Allow - Action: - - iam:AttachGroupPolicy - - iam:DetachGroupPolicy - Resource: - - !GetAtt DeploymentGroup.Arn - - - Sid: AllowGettingGroupInformation - Effect: Allow - Action: - - iam:GetGroup - Resource: !Sub arn:aws:iam::${AWS::AccountId}:group/${DeploymentGroup} - - DnsStackDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deployment of DNS stack - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowHostedZoneDeployment - Effect: Allow - Action: - - route53:CreateHostedZone - - route53:ListQueryLoggingConfigs - - route53:DeleteHostedZone - - route53:GetChange - - route53:ChangeTagsForResource - - route53:GetHostedZone - - route53:ChangeResourceRecordSets - Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/access-control-overview.html#access-control-manage-access-intro-resource-policies - - WebStackDeployPolicy: - # We need a role to run s3:PutBucketPolicy, IAM users cannot run it. See https://stackoverflow.com/a/48551383 - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows deployment of web stack - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowCloudFrontOAIDeployment - Effect: Allow - Action: - - cloudfront:GetCloudFrontOriginAccessIdentity - - cloudfront:CreateCloudFrontOriginAccessIdentity - - cloudfront:GetCloudFrontOriginAccessIdentityConfig - - cloudfront:DeleteCloudFrontOriginAccessIdentity - Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cf-api-permissions-ref.html - - - Sid: AllowCloudFrontDistributionDeployment - Effect: Allow - Action: - - cloudfront:CreateDistribution - - cloudfront:DeleteDistribution - - cloudfront:UpdateDistribution - - cloudfront:GetDistribution - - cloudfront:TagResource - - cloudfront:UpdateCloudFrontOriginAccessIdentity - Resource: !Sub arn:aws:cloudfront::${AWS::AccountId}:* - - - Sid: AllowS3BucketPolicyAccess - Effect: Allow - Action: - - s3:CreateBucket - - s3:DeleteBucket - - s3:PutBucketWebsite - - s3:DeleteBucketPolicy - - s3:PutBucketPolicy - - s3:GetBucketPolicy - Resource: !Sub arn:aws:s3:::${WebStackName}* - - - Sid: AllowRecordDeploymentToRoute53 - Effect: Allow - Action: - - route53:GetHostedZone - - route53:ChangeResourceRecordSets - - route53:GetChange - - route53:ListResourceRecordSets - Resource: '*' # Does not support resource-level permissions https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/access-control-overview.html#access-control-manage-access-intro-resource-policies - - S3SiteDeployPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows listing buckets to be able to list objects in a bucket - # ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resources - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowListingObjects - Effect: Allow - Action: - - s3:ListBucket # To allow ListObjectsV2 - Resource: !Sub arn:aws:s3:::${WebStackName}* - - - Sid: AllowUpdatingObjects - Effect: Allow - Action: - - s3:PutObject - - s3:DeleteObject - Resource: !Sub arn:aws:s3:::${WebStackName}*/* - - CloudFrontInvalidationPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows creating invalidations on CloudFront - # ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowCloudFrontInvalidations - Effect: Allow - Action: - - cloudfront:CreateInvalidation - Resource: "*" # Does not support resource-level permissions https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cf-api-permissions-ref.html - - StackExportReaderPolicy: - Type: AWS::IAM::ManagedPolicy - Properties: - Description: Allows creating invalidations on CloudFront - # ManagedPolicyName: Commented out because CloudFormation requires to rename when replacing custom-named resource - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowGettingBucketName - Effect: Allow - Action: - - cloudformation:DescribeStacks - Resource: !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/${WebStackName}/* - -Outputs: - ResolveCertificateLambdaRoleArn: - Description: The Amazon Resource Name (ARN) of the lambda for deploying certificates. - Value: !GetAtt ResolveCertificateLambdaRole.Arn - Export: - Name: !Join [ ':', [ !Ref 'AWS::StackName', ResolveCertificateLambdaRoleArn ] ] - - CertificateStackDeployRoleArn: - Description: "GitHub secret: AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt CertificateStackDeployRole.Arn - - DnsStackDeployRoleArn: - Description: "GitHub secret: AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt DnsStackDeployRole.Arn - - IamStackDeployRoleArn: - Description: "GitHub secret: AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt IamStackDeployRole.Arn - - WebStackDeployRoleArn: - Description: "GitHub secret: AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt WebStackDeployRole.Arn - - S3SiteDeployRoleArn: - Description: "GitHub secret: AWS_S3_SITE_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt S3SiteDeployRole.Arn - - CloudFrontSiteDeployRoleArn: - Description: "GitHub secret: AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN" - Value: !GetAtt CloudFrontSiteDeployRole.Arn diff --git a/aws/scripts/configure/create-role-profile.sh b/aws/scripts/configure/create-role-profile.sh deleted file mode 100644 index 98f127d0..00000000 --- a/aws/scripts/configure/create-role-profile.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --user-profile) USER_PROFILE="$2"; shift;; - --role-profile) ROLE_PROFILE="$2"; shift;; - --role-arn) ROLE_ARN="$2"; shift;; - --session) SESSION="$2";shift;; - --region) REGION="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$USER_PROFILE" ]; then echo "User profile name is not set."; exit 1; fi; -if [ -z "$ROLE_PROFILE" ]; then echo "Role profile name is not set."; exit 1; fi; -if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set"; exit 1; fi; -if [ -z "$SESSION" ]; then echo "Session name is not set."; exit 1; fi; -if [ -z "$REGION" ]; then echo "Region is not set."; exit 1; fi; - -creds=$(aws sts assume-role --role-arn $ROLE_ARN --role-session-name $SESSION --profile $USER_PROFILE) - -aws_access_key_id=$(echo $creds | jq -r '.Credentials.AccessKeyId') -echo ::add-mask::$aws_access_key_id -aws_secret_access_key=$(echo $creds | jq -r '.Credentials.SecretAccessKey') -echo ::add-mask::$aws_secret_access_key -aws_session_token=$(echo $creds | jq -r '.Credentials.SessionToken') -echo ::add-mask::$aws_session_token - -aws configure --profile $ROLE_PROFILE set aws_access_key_id $aws_access_key_id -aws configure --profile $ROLE_PROFILE set aws_secret_access_key $aws_secret_access_key -aws configure --profile $ROLE_PROFILE set aws_session_token $aws_session_token -aws configure --profile $ROLE_PROFILE set region $REGION - -echo Profile $ROLE_PROFILE is created - -bash "${BASH_SOURCE%/*}/mask-identity.sh" --profile $ROLE_PROFILE \ No newline at end of file diff --git a/aws/scripts/configure/create-user-profile.sh b/aws/scripts/configure/create-user-profile.sh deleted file mode 100644 index bd6591a7..00000000 --- a/aws/scripts/configure/create-user-profile.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --profile) PROFILE="$2"; shift;; - --access-key-id) ACCESS_KEY_ID="$2"; shift;; - --secret-access-key) SECRET_ACCESS_KEY="$2"; shift;; - --region) REGION="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$PROFILE" ]; then echo "Profile name is not set."; exit 1; fi; -echo $PROFILE -if [ -z "$ACCESS_KEY_ID" ]; then echo "Access key ID is not set"; exit 1; fi; -if [ -z "$SECRET_ACCESS_KEY" ]; then echo "Secret access key is not set."; exit 1; fi; -if [ -z "$REGION" ]; then echo "Region is not set."; exit 1; fi; - -aws configure --profile $PROFILE set aws_access_key_id $ACCESS_KEY_ID -aws configure --profile $PROFILE set aws_secret_access_key $SECRET_ACCESS_KEY -aws configure --profile $PROFILE set region $REGION - -echo Profile $PROFILE is created - -bash "${BASH_SOURCE%/*}/mask-identity.sh" --profile $PROFILE \ No newline at end of file diff --git a/aws/scripts/configure/mask-identity.sh b/aws/scripts/configure/mask-identity.sh deleted file mode 100644 index e71c4739..00000000 --- a/aws/scripts/configure/mask-identity.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --profile) PROFILE="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$PROFILE" ]; then echo "Profile name is not set."; exit 1; fi; - -aws_identity=$(aws sts get-caller-identity --profile $PROFILE) -echo ::add-mask::$(echo $aws_identity | jq -r '.Account') -echo ::add-mask::$(echo $aws_identity | jq -r '.UserId') -echo ::add-mask::$(echo $aws_identity | jq -r '.Arn') - -echo Credentials are masked \ No newline at end of file diff --git a/aws/scripts/deploy/deploy-stack.sh b/aws/scripts/deploy/deploy-stack.sh deleted file mode 100644 index 2b03a8f3..00000000 --- a/aws/scripts/deploy/deploy-stack.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --template-file) TEMPLATE_FILE="$2"; shift;; - --stack-name) STACK_NAME="$2"; shift;; - --profile) PROFILE="$2"; shift;; - --capabilities) CAPABILITY_IAM="$2"; shift;; - --role-arn) ROLE_ARN="$2";shift;; - --session) SESSION="$2";shift;; - --region) REGION="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$TEMPLATE_FILE" ]; then echo "Template file is not set."; exit 1; fi; -if [ -z "$STACK_NAME" ]; then echo "Template file is not set."; exit 1; fi; -if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi; -if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi; -if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi; - - -echo Validating stack "$STACK_NAME" -aws cloudformation validate-template \ - --template-body file://$TEMPLATE_FILE \ - --profile $PROFILE - -ROLE_PROFILE=$STACK_NAME - -echo Assuming role -bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \ - --role-profile $ROLE_PROFILE --user-profile $PROFILE \ - --role-arn $ROLE_ARN \ - --session $SESSION \ - --region $REGION - -echo Deploying stack "$TEMPLATE_FILE" -aws cloudformation deploy \ - --template-file $TEMPLATE_FILE \ - --stack-name $STACK_NAME \ - ${CAPABILITY_IAM:+ --capabilities $CAPABILITY_IAM} \ - --no-fail-on-empty-changeset \ - --profile $ROLE_PROFILE \ No newline at end of file diff --git a/aws/scripts/deploy/deploy-to-s3.sh b/aws/scripts/deploy/deploy-to-s3.sh deleted file mode 100644 index 20746de8..00000000 --- a/aws/scripts/deploy/deploy-to-s3.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --folder) FOLDER="$2"; shift;; - --web-stack-name) WEB_STACK_NAME="$2"; shift;; - --web-stack-s3-name-output-name) WEB_STACK_S3_NAME_OUTPUT_NAME="$2"; shift;; - --storage-class) STORAGE_CLASS="$2"; shift;; - --profile) PROFILE="$2"; shift;; - --role-arn) ROLE_ARN="$2";shift;; - --session) SESSION="$2";shift;; - --region) REGION="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$FOLDER" ]; then echo "Folder is not set."; exit 1; fi; -if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi; -if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi; -if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi; -if [ -z "$WEB_STACK_NAME" ]; then echo "Web stack name is not set."; exit 1; fi; -if [ -z "$WEB_STACK_S3_NAME_OUTPUT_NAME" ]; then echo "S3 name output name is not set."; exit 1; fi; -if [ -z "$STORAGE_CLASS" ]; then echo "S3 object storage class is not set."; exit 1; fi; - -echo Assuming role -ROLE_PROFILE=deploy-s3 -bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \ - --role-profile $ROLE_PROFILE --user-profile $PROFILE \ - --role-arn $ROLE_ARN \ - --session $SESSION \ - --region $REGION - -echo Getting S3 bucket name from stack "$WEB_STACK_NAME" with output "$WEB_STACK_S3_NAME_OUTPUT_NAME" -S3_BUCKET_NAME=$(aws cloudformation describe-stacks \ - --stack-name $WEB_STACK_NAME \ - --query "Stacks[0].Outputs[?OutputKey=='$WEB_STACK_S3_NAME_OUTPUT_NAME'].OutputValue" \ - --output text \ - --profile $ROLE_PROFILE) -if [ -z "$S3_BUCKET_NAME" ]; then echo "Could not read S3 bucket name"; exit 1; fi; -echo ::add-mask::$S3_BUCKET_NAME # Just being extra cautious - -echo Syncing folder to S3 - -aws s3 sync $FOLDER s3://$S3_BUCKET_NAME \ - --storage-class $STORAGE_CLASS \ - --no-progress --follow-symlinks --delete \ - --profile $ROLE_PROFILE \ No newline at end of file diff --git a/aws/scripts/deploy/invalidate-cloudfront-cache.sh b/aws/scripts/deploy/invalidate-cloudfront-cache.sh deleted file mode 100644 index 0e14258d..00000000 --- a/aws/scripts/deploy/invalidate-cloudfront-cache.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# Parse parameters -while [[ "$#" -gt 0 ]]; do case $1 in - --paths) PATHS="$2"; shift;; - --web-stack-name) WEB_STACK_NAME="$2"; shift;; - --web-stack-cloudfront-arn-output-name) WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME="$2"; shift;; - --profile) PROFILE="$2"; shift;; - --role-arn) ROLE_ARN="$2";shift;; - --session) SESSION="$2";shift;; - --region) REGION="$2";shift;; - *) echo "Unknown parameter passed: $1"; exit 1;; -esac; shift; done - -# Verify parameters -if [ -z "$PATHS" ]; then echo "Paths is not set."; exit 1; fi; -if [ -z "$PROFILE" ]; then echo "Profile is not set."; exit 1; fi; -if [ -z "$ROLE_ARN" ]; then echo "Role ARN is not set."; exit 1; fi; -if [ -z "$SESSION" ]; then echo "Role session is not set."; exit 1; fi; -if [ -z "$WEB_STACK_NAME" ]; then echo "Web stack name is not set."; exit 1; fi; -if [ -z "$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME" ]; then echo "CloudFront ARN output name is not set."; exit 1; fi; - - -echo Assuming role -ROLE_PROFILE=invalidate-cloudfront -bash "${BASH_SOURCE%/*}/../configure/create-role-profile.sh" \ - --role-profile $ROLE_PROFILE --user-profile $PROFILE \ - --role-arn $ROLE_ARN \ - --session $SESSION \ - --region $REGION - -echo Getting CloudFront ARN from stack "$WEB_STACK_NAME" with output "$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME" -CLOUDFRONT_ARN=$(aws cloudformation describe-stacks \ - --stack-name $WEB_STACK_NAME \ - --query "Stacks[0].Outputs[?OutputKey=='$WEB_STACK_CLOUDFRONT_ARN_OUTPUT_NAME'].OutputValue" \ - --output text \ - --profile $ROLE_PROFILE) -if [ -z "$CLOUDFRONT_ARN" ]; then echo "Could not read CloudFront ARN"; exit 1; fi; -echo ::add-mask::$CLOUDFRONT_ARN - -echo Syncing folder to S3 -aws cloudfront create-invalidation \ - --paths $PATHS \ - --distribution-id $CLOUDFRONT_ARN \ - --profile $ROLE_PROFILE \ No newline at end of file diff --git a/aws/web-stack.yaml b/aws/web-stack.yaml deleted file mode 100644 index 17049c30..00000000 --- a/aws/web-stack.yaml +++ /dev/null @@ -1,138 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' - -Description: |- - > Creates an S3 bucket configured for hosting a static webpage. - > Creates CloudFront distribution that has access to read the S3 bucket. - -Parameters: - - RootDomainName: - Type: String - Default: privacy.sexy - Description: The root DNS name of the website e.g. privacy.sexy - AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?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 \ No newline at end of file diff --git a/docs/aws-cloudformation.png b/docs/aws-cloudformation.png deleted file mode 100644 index 5d9e8890..00000000 Binary files a/docs/aws-cloudformation.png and /dev/null differ