diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..1292679 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,84 @@ +name: Main + +on: + workflow_dispatch: + push: + branches: + - 'main' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + + - name: Assume AWS Credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + + - name: Install AWS SAM + uses: aws-actions/setup-sam@v2 + + # FIXME: We're only validating the top-level template and not the rest. + - name: Validate SAM template + run: sam validate --lint -t template.yaml + + - name: Validate StackSets SAM template (DNS Zone) + run: sam validate --lint -t stacksets/dns-zone/stackset.yaml + + - name: Synethsize StackSet templates + run: | + for _f in $(find . -type f -name 'template.yaml'); do + _dir="$(dirname $_f)/" \ + yq \ + -i \ + '(.. | select(has("localTemplateFile")) | .localTemplateFile) |= load_str(strenv(_dir) + .)' \ + $_f; + done + + - name: Package SAM artifact (Stacksets Template) + id: package-sam-stacksets + uses: ServerlessOpsIO/gha-package-aws-sam@v1 + with: + packaged_template_file: packaged-template.yaml + + - name: Store Artifacts + uses: ServerlessOpsIO/gha-store-artifacts@v1 + + + deploy_stacksets: + needs: + - build + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Setup job workspace + uses: ServerlessOpsIO/gha-setup-workspace@v1 + with: + checkout_artifact: true + + - name: Assume AWS Credentials + uses: ServerlessOpsIO/gha-assume-aws-credentials@v1 + with: + build_aws_account_id: ${{ secrets.AWS_CICD_ACCOUNT_ID }} + gha_build_role_name: ${{ secrets.AWS_CICD_BUILD_ROLE_NAME }} + deploy_aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + gha_deploy_role_name: ${{ secrets.AWS_CICD_DEPLOY_ROLE_NAME }} + + - name: Deploy via AWS SAM + uses: ServerlessOpsIO/gha-deploy-aws-sam@v1 + with: + aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + template_file: packaged-template.yaml + cfn_capabilities: CAPABILITY_AUTO_EXPAND + env_json: ${{ toJson(env) }} + secrets_json: ${{ toJson(secrets) }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5901e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,245 @@ +.aws-sam/ + +# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Build folder + +*/build/* + +# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6cd1de --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# AWS DNS Management + +Manage DNS zone and records. \ No newline at end of file diff --git a/cfn-parameters.json b/cfn-parameters.json new file mode 100644 index 0000000..df6e8b1 --- /dev/null +++ b/cfn-parameters.json @@ -0,0 +1,7 @@ +{ + "RootDomainName": "serverlessops.io", + "DnsManagementAccountId": "$secrets.DNS_ROOT_ZONE_ACCOUNT_ID", + "TargetOuIds": $secrets.AWS_ORG_ROOT_ID, + "TargetAccountIds": $secrets.DEPLOYMENT_ACCOUNT_ID, + "TargetRegions": "us-east-1" +} \ No newline at end of file diff --git a/cfn-tags.json b/cfn-tags.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/cfn-tags.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/samconfig.toml b/samconfig.toml new file mode 100644 index 0000000..e78dc5a --- /dev/null +++ b/samconfig.toml @@ -0,0 +1,31 @@ +# More information about the configuration file can be found here: +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html +version = 0.1 + +[default] +[default.global.parameters] +stack_name = "aws-dns-management" + +[default.build.parameters] +cached = true +parallel = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_AUTO_EXPAND" +confirm_changeset = false +#resolve_s3 = true + +[default.package.parameters] +#resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/stacksets/cfn-cr/stackset.yaml b/stacksets/cfn-cr/stackset.yaml new file mode 100644 index 0000000..e85ae3d --- /dev/null +++ b/stacksets/cfn-cr/stackset.yaml @@ -0,0 +1,128 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: CFN DNS Record Automation + +Parameters: + CustomResourceTopicName: + Type: String + Description: Name of the SNS topic for custom resources + DnsRootZoneId: + Type: String + Description: Hosted Zone ID + DnsRootZoneAccountId: + Type: String + Description: DNS Root Zone Account ID + ManageDnsZoneIamRole: + Type: String + Description: IAM role name for RegisterDnsZone + +Resources: + ServerlessOps: + Type: "AWS::Route53::HostedZone" + Properties: + Name: "serverlessops.io" + HostedZoneConfig: + Comment: ServerlessOps + + ServerlessOpsRR: + Type: "AWS::Route53::RecordSetGroup" + Properties: + Comment: "Apex Records" + HostedZoneId: !Ref ServerlessOps + RecordSets: + - Name: "serverlessops.io." + Type: "MX" + TTL: 300 + ResourceRecords: + - 1 aspmx.l.google.com + - 5 alt1.aspmx.l.google.com + - 5 alt2.aspmx.l.google.com + - 10 alt3.aspmx.l.google.com + - 10 alt4.aspmx.l.google.com + - 15 n2235kmtje5chj5uz63i3sluem7gtehtp5dlmladr2nfntojbqpq.mx-verification.google.com. + - Name: "serverlessops.io" + Type: "TXT" + TTL: 14400 + ResourceRecords: + - "\"v=spf1 include:_spf.google.com include:277116.spf08.hubspotemail.net ~all\"" + + + # WWW + ServerlessOpsWww: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "Website" + HostedZoneId: !Ref ServerlessOps + Name: "www.serverlessops.io." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - 277116.group16.sites.hubspot.net + + # Hubspot + ServerlessOpsDomainKeyHs1: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: "hs1._domainkey.serverlessops.io." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - serverlessops-io.hs01a.dkim.hubspotemail.net + + ServerlessOpsDomainKeyHs2: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: "hs2._domainkey.serverlessops.io." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - serverlessops-io.hs01b.dkim.hubspotemail.net + + ServerlessOpsDomainKeySmtpapi: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: "smtpapi._domainkey.serverlessops.io." + Type: "TXT" + TTL: 3600 + # Those escape secquences are necessary. + ResourceRecords: + - "\"k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB\"" + + + # Automation + RegisterDnsZoneFunction: + Type: AWS::Serverless::Function + Properties: + Handler: function.handler + Runtime: python3.12 + CodeUri: ../../src/handlers/RegisterDnsZone + Description: Register DNS Sub Zone + MemorySize: 128 + Timeout: 10 + Environment: + Variables: + DNS_ROOT_ZONE_ID: !Ref DnsRootZoneId + DNS_ROOT_ZONE_ACCOUNT_ID: !Ref DnsRootZoneAccountId + CROSS_ACCOUNT_IAM_ROLE_NAME: !Sub "ManageDnsZoneIamRole" + Events: + RegisterDnsSubZone: + Type: SNS + Properties: + Topic: !Ref CustomResourceTopic + FilterPolicyScope: MessageBody + FilterPolicy: + ResourceType: + - Custom::RegisterDnsZone + Policies: + - Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - sts:AssumeRole + Resource: !Sub arn:aws:iam::${DnsRootZoneAccountId}:role/${ManageDnsZoneIamRole} \ No newline at end of file diff --git a/stacksets/dns-zone/stackset.yaml b/stacksets/dns-zone/stackset.yaml new file mode 100644 index 0000000..6f25000 --- /dev/null +++ b/stacksets/dns-zone/stackset.yaml @@ -0,0 +1,85 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: DNS Domain + +Parameters: + RootDomainName: + Type: String + Description: Root domain name + +Resources: + ServerlessOps: + Type: "AWS::Route53::HostedZone" + Properties: + Name: !Ref RootDomainName + HostedZoneConfig: + Comment: ServerlessOps + + ServerlessOpsRR: + Type: "AWS::Route53::RecordSetGroup" + Properties: + Comment: "Apex Records" + HostedZoneId: !Ref ServerlessOps + RecordSets: + - Name: !Sub "${RootDomainName}." + Type: "MX" + TTL: 300 + ResourceRecords: + - 1 aspmx.l.google.com + - 5 alt1.aspmx.l.google.com + - 5 alt2.aspmx.l.google.com + - 10 alt3.aspmx.l.google.com + - 10 alt4.aspmx.l.google.com + - 15 n2235kmtje5chj5uz63i3sluem7gtehtp5dlmladr2nfntojbqpq.mx-verification.google.com. + - Name: !Ref RootDomainName + Type: "TXT" + TTL: 14400 + ResourceRecords: + - "\"v=spf1 include:_spf.google.com include:277116.spf08.hubspotemail.net ~all\"" + + + # WWW + ServerlessOpsWww: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "Website" + HostedZoneId: !Ref ServerlessOps + Name: !Sub "www.${RootDomainName}." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - 277116.group16.sites.hubspot.net + + # Hubspot + ServerlessOpsDomainKeyHs1: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: !Sub "hs1._domainkey.${RootDomainName}." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - serverlessops-io.hs01a.dkim.hubspotemail.net + + ServerlessOpsDomainKeyHs2: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: !Sub "hs2._domainkey.${RootDomainName}." + Type: "CNAME" + TTL: 3600 + ResourceRecords: + - serverlessops-io.hs01b.dkim.hubspotemail.net + + ServerlessOpsDomainKeySmtpapi: + Type: "AWS::Route53::RecordSet" + Properties: + Comment: "HubSpot domain key" + HostedZoneId: !Ref ServerlessOps + Name: !Sub "smtpapi._domainkey.${RootDomainName}." + Type: "TXT" + TTL: 3600 + # Those escape secquences are necessary. + ResourceRecords: + - "\"k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDPtW5iwpXVPiH5FzJ7Nrl8USzuY9zqqzjE0D1r04xDN6qwziDnmgcFNNfMewVKN2D1O+2J9N14hRprzByFwfQW76yojh54Xu3uSbQ3JP0A7k8o8GutRF8zbFUA8n0ZH2y0cIEjMliXY4W4LwPA7m4q0ObmvSjhd63O9d8z1XkUBwIDAQAB\"" diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..3e3f6f6 --- /dev/null +++ b/template.yaml @@ -0,0 +1,47 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: DNS Domain Management + +Metadata: + DnsRoot: + localTemplateFile: &dns_zone_template_body ./stacksets/dns-zone/stackset.yaml + +Parameters: + TargetOuIds: + Type: CommaDelimitedList + Description: Target deployment OU IDs + TargetAccountIds: + Type: CommaDelimitedList + Description: Target Accounts + TargetRegions: + Type: CommaDelimitedList + Description: Target deployment region + RootDomainName: + Type: String + Description: Root domain name + +Resources: + DnsRootDomain: + Type: AWS::CloudFormation::StackSet + Properties: + StackSetName: DnsRootDomain + Description: DNS Root Domain + Parameters: + - ParameterKey: RootDomainName + ParameterValue: !Ref RootDomainName + StackInstancesGroup: + - DeploymentTargets: + AccountFilterType: INTERSECTION + OrganizationalUnitIds: !Ref TargetOuIds + Accounts: !Ref TargetAccountIds + Regions: !Ref TargetRegions + AutoDeployment: + Enabled: true + RetainStacksOnAccountRemoval: false + ManagedExecution: + Active: true + OperationPreferences: + RegionConcurrencyType: PARALLEL + FailureToleranceCount: 1 + MaxConcurrentCount: 5 + PermissionModel: SERVICE_MANAGED + TemplateBody: *dns_zone_template_body \ No newline at end of file