diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a1c8874 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +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 + + + - name: Validate template + run: sam validate --lint + + - name: Build artifact + run: sam build --parallel --template template.yaml + + - name: Store Artifacts + uses: ServerlessOpsIO/gha-store-artifacts@v1 + with: + use_aws_sam: true + + deploy_sandbox: + needs: + - build + + environment: sandbox + 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 }} + deploy_aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + + - name: Deploy via AWS SAM + uses: ServerlessOpsIO/gha-deploy-aws-sam@v1 + with: + aws_account_id: ${{ secrets.DEPLOYMENT_ACCOUNT_ID }} + env_json: ${{ toJson(env) }} + secrets_json: ${{ toJson(secrets) }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e421440 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# 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 +MANIFEST + +# 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 +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Dev +.mypy_cache/ + +# pyenv / environments +.python-version +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.settings/ +.project +.pydevproject +.vscode/ +*.code-workspace + +# Mac Cruft +.DS_Store + +# Deploy +codepipeline-config-*.yaml + + +# AWS SAM +.aws-sam/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fbd1d2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Backstage Cluster + +ECS Cluster for Backstage diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..e6f8d24 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.component_name }} + description: ${{ values.component_description }} + namespace: ${{ values.component_namespace }} + annotations: + github.com/project-slug: NBCUniversal/${{ values.component_name }} + backstage.io/techdocs-ref: dir:. +spec: + type: aws_ecs_cluster + lifecycle: production + owner: ${{ values.owner }} + system: ${{ values.system_entity }} + domain: ${{ values.domain_entity }} diff --git a/cfn-parameters.json b/cfn-parameters.json new file mode 100644 index 0000000..47469cd --- /dev/null +++ b/cfn-parameters.json @@ -0,0 +1,10 @@ +{ + "Domain": "Infrastructure", + "System": "Backstage", + "Component": $env.GITHUB_REPOSITORY_NAME_PART_SLUG_CS, + "CodeBranch": $env.GITHUB_REF_SLUG_CS, + "VpcId": "/org/networking/VpcId", + "VpcSubnets": "/org/networking/VpcPublicSubnets", + "Hostname": "backstage.serverlessops.io", + "DnsZoneId": "/org/dns/ZoneId" +} \ No newline at end of file diff --git a/cfn-tags.json b/cfn-tags.json new file mode 100644 index 0000000..7dac9a2 --- /dev/null +++ b/cfn-tags.json @@ -0,0 +1,6 @@ +{ + "org:domain": "Infrastructure", + "org:system": "Backstage", + "org:component": $env.GITHUB_REPOSITORY_NAME_PART_SLUG_CS, + "org:code-branch": $env.GITHUB_REF_SLUG_CS, +} \ No newline at end of file diff --git a/samconfig.toml b/samconfig.toml new file mode 100644 index 0000000..73c1ec3 --- /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 = "backstage-cluster" + +[default.build.parameters] +cached = true +parallel = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_NAMED_IAM 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" \ No newline at end of file diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..41b84e5 --- /dev/null +++ b/template.yaml @@ -0,0 +1,182 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + Backstage Cluster + + Backstage ECS cluster for running the Backstage application + +Parameters: + Domain: + Type: String + Description: 'Application Platform' + + System: + Type: String + Description: 'Application System' + + Component: + Type: String + Description: 'Application Component' + + CodeBranch: + Type: String + Description: "Name of deployment branch" + + VpcId: + Type: AWS::SSM::Parameter::Value + Description: Account VPC ID + + VpcSubnets: + Type: AWS::SSM::Parameter::Value + Description: Account subnets + + Hostname: + Type: String + Description: Site FQDN + + DnsZoneId: + Type: AWS::SSM::Parameter::Value + Description: Route53 Hosted Zone ID + + + +Resources: + # ALB Resources + ## ALB networking + EcsAlbSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to ALB for Fargate + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + CidrIp: 0.0.0.0/0 + FromPort: 443 + ToPort: 443 + + SiteDnsRecord: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref DnsZoneId + Name: !Ref Hostname + Type: A + AliasTarget: + DNSName: !GetAtt EcsAlb.DNSName + HostedZoneId: !GetAtt EcsAlb.CanonicalHostedZoneID + + SiteCertificate: + Type: AWS::CertificateManager::Certificate + Properties: + DomainName: !Ref Hostname + ValidationMethod: DNS + + ## ALB setup + EcsAlb: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Type: application + Scheme: internet-facing + Subnets: !Ref VpcSubnets + SecurityGroups: + - !Ref EcsAlbSecurityGroup + + EcsAlbListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + - Type: fixed-response + Order: 50000 + FixedResponseConfig: + ContentType: application/json + StatusCode: 200 + MessageBody: '{ "healthy": true }' + LoadBalancerArn: !Ref 'EcsAlb' + Port: 443 + Protocol: HTTPS + Certificates: + - CertificateArn: !Ref SiteCertificate + + # ECS Resources + ## Cluster + EcsCluster: + Type: AWS::ECS::Cluster + + EcsTaskExecutionIamRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: AmazonECSTaskExecutionRolePolicy + PolicyDocument: + Statement: + - Effect: Allow + Action: + # Allow the ECS Tasks to download images from ECR + - 'ecr:GetAuthorizationToken' + - 'ecr:BatchCheckLayerAvailability' + - 'ecr:GetDownloadUrlForLayer' + - 'ecr:BatchGetImage' + + # Allow the ECS tasks to upload logs to CloudWatch + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: '*' + + ## ECS Network Access + ContainerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Fargate container access + VpcId: !Ref 'VpcId' + + ContainerAlbSecurityGroupIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from ALBs + GroupId: !Ref ContainerSecurityGroup + # All TCP ports + IpProtocol: tcp + FromPort: 80 + ToPort: 65535 + SourceSecurityGroupId: !Ref EcsAlbSecurityGroup + + # SSM Values + EcsClusterNameSsmParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Description: Name of ECS Cluster + Name: !Sub /${Domain}/${System}/${Component}/${CodeBranch}/EcsClusterName + Value: !Ref EcsCluster + + EcsTaskExecutionIamRoleArn: + Type: AWS::SSM::Parameter + Properties: + Type: String + Description: ARN of IAM role for performing ECS related operations by tasks + Name: !Sub /${Domain}/${System}/${Component}/${CodeBranch}/EcsTaskExecutionIamRoleArn + Value: !Ref EcsTaskExecutionIamRole + + ContainerSecurityGroupIdSsmParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Description: Name of ECS Cluster + Name: !Sub /${Domain}/${System}/${Component}/${CodeBranch}/ContainerSecurityGroupId + Value: !Ref ContainerSecurityGroup + + EcsAlbListenerArnSsmParam: + Type: AWS::SSM::Parameter + Properties: + Type: String + Description: ARN of cluster ALB + Name: !Sub /${Domain}/${System}/${Component}/${CodeBranch}/EcsAlbListenerArn + Value: !Ref EcsAlbListener