├── .dockerignore ├── .env.template ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── app-config.yaml ├── catalog-info.yaml ├── docker-compose.yml ├── entrypoint.sh ├── lerna.json ├── package.json ├── packages ├── app │ ├── .eslintrc.js │ ├── cypress.json │ ├── cypress │ │ ├── .eslintrc.json │ │ └── integration │ │ │ └── app.js │ ├── package.json │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── safari-pinned-tab.svg │ └── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis.ts │ │ ├── components │ │ ├── Root │ │ │ ├── LogoFull.tsx │ │ │ ├── LogoIcon.tsx │ │ │ ├── Root.tsx │ │ │ └── index.ts │ │ ├── catalog │ │ │ └── EntityPage.tsx │ │ └── search │ │ │ └── SearchPage.tsx │ │ ├── index.tsx │ │ └── setupTests.ts └── backend │ ├── .eslintrc.js │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── src │ ├── index.test.ts │ ├── index.ts │ ├── plugins │ ├── app.ts │ ├── auth.ts │ ├── catalog.ts │ ├── proxy.ts │ ├── scaffolder.ts │ ├── search.ts │ └── techdocs.ts │ └── types.ts ├── terraform ├── .terraform.lock.hcl ├── Dockerfile ├── main.tf ├── modules │ ├── alb │ │ ├── alb.tf │ │ ├── output.tf │ │ └── variables.tf │ ├── ecs │ │ ├── ecs.tf │ │ └── variables.tf │ ├── iam │ │ ├── iam.tf │ │ ├── output.tf │ │ └── variables.tf │ ├── rds │ │ ├── output.tf │ │ ├── rds.tf │ │ └── variables.tf │ ├── s3 │ │ ├── output.tf │ │ ├── s3.tf │ │ └── variables.tf │ ├── ssm │ │ ├── output.tf │ │ ├── ssm.tf │ │ └── variables.tf │ └── vpc │ │ ├── output.tf │ │ ├── variables.tf │ │ └── vpc.tf ├── terraform.tf ├── terraform.tfvars └── variables.tf ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | NPM_REGISTRY=https://registry.npmjs.org 2 | POSTGRES_DB=db 3 | POSTGRES_HOST=localhost 4 | POSTGRES_PORT=5432 5 | POSTGRES_USER=db_user 6 | POSTGRES_PASSWORD=db_password 7 | AWS_DEFAULT_REGION=eu-west-1 8 | AWS_ACCESS_KEY_ID= 9 | AWS_SECRET_ACCESS_KEY= 10 | BUCKET_NAME= 11 | GITHUB_TOKEN= 12 | AUTH_GITHUB_CLIENT_ID= 13 | AUTH_GITHUB_CLIENT_SECRET= 14 | 15 | # Terraform variables 16 | TF_VAR_docker_image_tag=1.0.0 17 | TF_VAR_postgres_user=db_user 18 | TF_VAR_postgres_password=db_password 19 | TF_VAR_github_token= 20 | TF_VAR_github_client_id= 21 | TF_VAR_github_client_secret= 22 | TF_VAR_access_key_id= 23 | TF_VAR_secret_access_key= 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Coverage directory generated when running tests with coverage 13 | coverage 14 | 15 | # Dependencies 16 | node_modules/ 17 | 18 | # Node version directives 19 | .nvmrc 20 | 21 | # dotenv environment variables file 22 | .env 23 | .env.test 24 | 25 | # Build output 26 | dist 27 | dist-types 28 | 29 | # Temporary change files created by Vim 30 | *.swp 31 | 32 | # MkDocs build output 33 | site 34 | 35 | # Local configuration files 36 | *.local.yaml 37 | 38 | # Sensitive credentials 39 | *-credentials.yaml 40 | 41 | # Terraform 42 | .terraform 43 | *.tfstate 44 | *.tfstate.backup 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-buster-slim 2 | 3 | ENV NPM_VERSION=6.14.12 4 | ARG NPM_REGISTRY="https://registry.npmjs.org" 5 | 6 | WORKDIR /usr/src/app 7 | 8 | # Install gettext-base to use envsubst 9 | RUN apt-get update && apt-get install gettext-base 10 | 11 | # Fix openjdk-11-jdk-headless error 12 | RUN mkdir -p /usr/share/man/man1 13 | 14 | # Install cookiecutter 15 | RUN apt-get install -y python3 python3-dev python3-pip python-matplotlib g++ \ 16 | gcc musl-dev openjdk-11-jdk-headless curl graphviz ttf-dejavu fontconfig 17 | 18 | # Download plantuml file, Validate checksum & Move plantuml file 19 | RUN curl -o plantuml.jar -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2021.4.jar/download \ 20 | && echo "be498123d20eaea95a94b174d770ef94adfdca18 plantuml.jar" | sha1sum -c - && mv plantuml.jar /opt/plantuml.jar 21 | 22 | # Install cookiecutter and mkdocs 23 | RUN pip3 install cookiecutter && pip3 install mkdocs-techdocs-core==0.0.16 24 | 25 | RUN apt-get remove -y --auto-remove curl 26 | 27 | # Create script to call plantuml.jar from a location in path 28 | RUN echo $'#!/bin/sh\n\njava -jar '/opt/plantuml.jar' ${@}' >> /usr/local/bin/plantuml 29 | RUN chmod 755 /usr/local/bin/plantuml 30 | 31 | # Install dependencies and update npm 32 | RUN npm config set registry ${NPM_REGISTRY} \ 33 | && npm config set strict-ssl false \ 34 | && yarn config set registry ${NPM_REGISTRY} \ 35 | && yarn config set strict-ssl false \ 36 | && npm install -g npm@${NPM_VERSION} 37 | 38 | COPY package.json yarn.lock /usr/src/app/ 39 | 40 | RUN cd /usr/src/app && yarn install --frozen-lockfile 41 | 42 | # Expose ports 43 | EXPOSE 3000 7000 44 | 45 | # Configure the entrypoint script. 46 | COPY entrypoint.sh /entrypoint.sh 47 | RUN chmod +x /entrypoint.sh 48 | 49 | # Copy the app. 50 | COPY . /usr/src/app 51 | 52 | ENTRYPOINT ["/entrypoint.sh"] 53 | CMD ["yarn", "dev"] 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | uname_S := $(shell uname -s) 2 | 3 | all: check-env-file build build-terraform-cli up 4 | 5 | check-env-file: 6 | @test -f .env || { echo ".env file does not exists. You can create one starting from env.template"; exit 1; } 7 | 8 | build: 9 | docker-compose build 10 | @echo "Application has been built succesfully." 11 | 12 | build-terraform-cli: 13 | docker build -t backstage/terraform-cli ./terraform 14 | 15 | up: 16 | docker-compose down -v 17 | docker-compose up -d 18 | 19 | cli: 20 | docker-compose run --rm app bash 21 | 22 | terraform-cli: 23 | docker run --rm -it --workdir /app \ 24 | --entrypoint bash -v $${PWD}/terraform:/app \ 25 | --env-file .env \ 26 | backstage/terraform-cli 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Backstage](https://backstage.io) Terraform demo 2 | 3 | This is your newly scaffolded Backstage App, Good Luck! 4 | 5 | ## Local environment 6 | To start the app, create your .env file from the `.env.template` file and insert these required env variables: 7 | 8 | - your AWS credentials 9 | - AWS_ACCESS_KEY_ID 10 | - AWS_SECRET_ACCESS_KEY 11 | - BUCKET_NAME: the name of the bucket for techdocs 12 | - GITHUB_TOKEN: your Github token to allow Backstage to connect to your repositories 13 | - your Github Oauth app for Backstage authentication: 14 | - AUTH_GITHUB_CLIENT_ID 15 | - AUTH_GITHUB_CLIENT_SECRET 16 | 17 | Then run: 18 | 19 | ```sh 20 | make 21 | ``` 22 | This will build the docker image and start the containers for the application and the database, after the build finishes you can visit the application at: 23 | ```sh 24 | localhost:3000 25 | ``` 26 | 27 | ## Infrastructure 28 | This demo uses Terraform to define and manage the AWS infrastructure that Backstage will use. 29 | All the Terraform files are in the `terraform` directory, here's a list of the modules and the relative services: 30 | - `vpc`: 1 VPC, 2 subnets, 1 internet gateway and 1 route table 31 | - `alb`: 1 security group and 1 Application Load Balancer 32 | - `rds`: 1 security group and 1 RDS PostgreSQL instance 33 | - `s3`: 1 private bucket 34 | - `ssm`: 8 Parameter Store secrets 35 | - `iam`: 3 policies, 1 role 36 | - `ecr`: 1 ECR repository 37 | - `ecs`: 1 cluster, 1 Cloudwatch log group, 1 task definition and 1 service 38 | 39 | 40 | ## AWS deploy 41 | To deploy your Backstage application on AWS with Terraform you must first set these env variables: 42 | - TF_VAR_github_token 43 | - TF_VAR_github_client_id 44 | - TF_VAR_github_client_secret 45 | - TF_VAR_access_key_id 46 | - TF_VAR_secret_access_key 47 | 48 | Then run `make` to build the Terraform container 49 | 50 | To let Terraform work, you need to manually create an S3 bucket in which Terraform will save the state of your infrastructure. 51 | Once created, save the bucket name by replacing the `{{BUCKET-NAME}}` placeholder in the `terraform / terraform.tf` file. 52 | 53 | The setup is now complete. To deploy, enter the terraform container by typing `make terraform-cli` and then: 54 | ```sh 55 | terraform init 56 | terraform apply 57 | ``` 58 | 59 | As soon as Terraform is done, build your Backstage Docker image and push it on the Elastic Container Registry. 60 | To build and tag the image run: 61 | ```sh 62 | docker build . -f packages/backend/Dockerfile --tag backstage 63 | docker tag backstage {{AWS-ACCOUNT-ID}}.dkr.ecr.eu-west-1.amazonaws.com/backstage-image:1.0.0 64 | ``` 65 | 66 | Then login on ECR with: 67 | ```sh 68 | aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin {{AWS-ACCOUNT-ID}}.dkr.ecr.eu-west-1.amazonaws.com 69 | ``` 70 | 71 | To push the image run: 72 | ```sh 73 | docker push {{AWS-ACCOUNT-ID}}.dkr.ecr.eu-west-1.amazonaws.com/backstage-image:1.0.0 74 | ``` 75 | > remember to replace `{{AWS-ACCOUNT-ID}}` with your AWS account id! 76 | 77 | After that, you should wait a few minutes for ECS to be up and running, then you can visit the application by typing the URL of your Load Balancer. 78 | 79 | Enjoy! -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Backstage App 3 | baseUrl: ${APP_DOMAIN} 4 | 5 | organization: 6 | name: Sparkfabrik 7 | 8 | backend: 9 | baseUrl: ${APP_DOMAIN} 10 | listen: 11 | port: 7000 12 | csp: 13 | connect-src: ["'self'", 'http:', 'https:'] 14 | cors: 15 | origin: ${APP_DOMAIN} 16 | methods: [GET, POST, PUT, DELETE] 17 | credentials: true 18 | database: 19 | client: pg 20 | connection: 21 | host: ${POSTGRES_HOST} 22 | port: ${POSTGRES_PORT} 23 | user: ${POSTGRES_USER} 24 | password: ${POSTGRES_PASSWORD} 25 | 26 | integrations: 27 | github: 28 | - host: github.com 29 | token: ${GITHUB_TOKEN} 30 | 31 | techdocs: 32 | builder: 'external' 33 | generators: 34 | techdocs: 'local' 35 | publisher: 36 | type: 'awsS3' 37 | awsS3: 38 | bucketName: ${BUCKET_NAME} 39 | region: ${DEFAULT_REGION} 40 | credentials: 41 | accessKeyId: ${ACCESS_KEY_ID} 42 | secretAccessKey: ${SECRET_ACCESS_KEY} 43 | 44 | auth: 45 | environment: development 46 | providers: 47 | github: 48 | development: 49 | clientId: ${AUTH_GITHUB_CLIENT_ID} 50 | clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} 51 | 52 | scaffolder: 53 | github: 54 | visibility: private 55 | 56 | catalog: 57 | rules: 58 | - allow: [Component, System, API, Group, User, Resource, Location] 59 | locations: 60 | # Backstage example components 61 | - type: url 62 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml 63 | 64 | # Backstage example systems 65 | - type: url 66 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-systems.yaml 67 | 68 | # Backstage example APIs 69 | - type: url 70 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml 71 | 72 | # Backstage example resources 73 | - type: url 74 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml 75 | 76 | # Backstage example organization groups 77 | - type: url 78 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme/org.yaml 79 | 80 | # Backstage example templates 81 | - type: url 82 | target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml 83 | rules: 84 | - allow: [Template] 85 | - type: url 86 | target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml 87 | rules: 88 | - allow: [Template] 89 | - type: url 90 | target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml 91 | rules: 92 | - allow: [Template] 93 | - type: url 94 | target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml 95 | rules: 96 | - allow: [Template] 97 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: backstage 5 | description: An example of a Backstage application. 6 | spec: 7 | type: website 8 | owner: john@example.com 9 | lifecycle: experimental 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | context: . 6 | args: 7 | NPM_REGISTRY: ${NPM_REGISTRY} 8 | volumes: 9 | - .:/usr/src/app 10 | ports: 11 | - 127.0.0.1:3000:3000 12 | - 7000:7000 13 | network_mode: 'host' 14 | environment: 15 | - NPM_REGISTRY=${NPM_REGISTRY} 16 | - POSTGRES_HOST=${POSTGRES_HOST} 17 | - POSTGRES_PORT=${POSTGRES_PORT} 18 | - POSTGRES_USER=${POSTGRES_USER} 19 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 20 | - AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID} 21 | - AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET} 22 | - GITHUB_TOKEN=${GITHUB_TOKEN} 23 | - BUCKET_NAME=${BUCKET_NAME} 24 | - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} 25 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 26 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 27 | depends_on: 28 | - db 29 | db: 30 | image: postgres:13.2 31 | ports: 32 | - 5432:5432 33 | environment: 34 | POSTGRES_DB: ${POSTGRES_HOST} 35 | POSTGRES_USER: ${POSTGRES_USER} 36 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 37 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | shopt -s nullglob 4 | BASE=${PWD} 5 | 6 | if [ "${1}" = 'yarn' ]; then 7 | echo "Installing yarn libraries..." 8 | cd ${BASE} && yarn install --frozen-lockfile 9 | fi 10 | 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "plugins/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "0.1.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backstage-terraform-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "12 || 14" 7 | }, 8 | "scripts": { 9 | "dev": "concurrently \"yarn start\" \"yarn start-backend\"", 10 | "start": "yarn workspace app start", 11 | "start-backend": "yarn workspace backend start", 12 | "build": "lerna run build", 13 | "build-image": "yarn workspace backend build-image", 14 | "tsc": "tsc", 15 | "tsc:full": "tsc --skipLibCheck false --incremental false", 16 | "clean": "backstage-cli clean && lerna run clean", 17 | "diff": "lerna run diff --", 18 | "test": "lerna run test --since origin/master -- --coverage", 19 | "test:all": "lerna run test -- --coverage", 20 | "lint": "lerna run lint --since origin/master --", 21 | "lint:all": "lerna run lint --", 22 | "create-plugin": "backstage-cli create-plugin --scope internal --no-private", 23 | "remove-plugin": "backstage-cli remove-plugin" 24 | }, 25 | "resolutions": { 26 | "graphql-language-service-interface": "2.8.2", 27 | "graphql-language-service-parser": "1.9.0" 28 | }, 29 | "workspaces": { 30 | "packages": [ 31 | "packages/*", 32 | "plugins/*" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "@backstage/cli": "^0.7.3", 37 | "@spotify/prettier-config": "^7.0.0", 38 | "concurrently": "^6.0.0", 39 | "lerna": "^4.0.0", 40 | "prettier": "^1.19.1" 41 | }, 42 | "prettier": "@spotify/prettier-config", 43 | "lint-staged": { 44 | "*.{js,jsx,ts,tsx}": [ 45 | "eslint --fix", 46 | "prettier --write" 47 | ], 48 | "*.{json,md}": [ 49 | "prettier --write" 50 | ] 51 | }, 52 | "jest": { 53 | "transformModules": [ 54 | "@asyncapi/react-component" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@backstage/cli/config/eslint')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3001", 3 | "fixturesFolder": false, 4 | "pluginsFile": false 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["cypress"], 3 | "extends": ["plugin:cypress/recommended"], 4 | "rules": { 5 | "jest/expect-expect": [ 6 | "error", 7 | { 8 | "assertFunctionNames": ["expect", "cy.contains"] 9 | } 10 | ], 11 | "import/no-extraneous-dependencies": [ 12 | "error", 13 | { 14 | "devDependencies": true, 15 | "optionalDependencies": true, 16 | "peerDependencies": true, 17 | "bundledDependencies": true 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/app/cypress/integration/app.js: -------------------------------------------------------------------------------- 1 | describe('App', () => { 2 | it('should render the catalog', () => { 3 | cy.visit('/'); 4 | cy.contains('My Company Catalog'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "bundled": true, 6 | "dependencies": { 7 | "@backstage/catalog-model": "^0.8.4", 8 | "@backstage/cli": "^0.7.3", 9 | "@backstage/core-app-api": "^0.1.4", 10 | "@backstage/core-components": "^0.1.4", 11 | "@backstage/core-plugin-api": "^0.1.3", 12 | "@backstage/integration-react": "^0.1.4", 13 | "@backstage/plugin-api-docs": "^0.6.0", 14 | "@backstage/plugin-catalog": "^0.6.5", 15 | "@backstage/plugin-catalog-import": "^0.5.11", 16 | "@backstage/plugin-catalog-react": "^0.2.5", 17 | "@backstage/plugin-github-actions": "^0.4.11", 18 | "@backstage/plugin-org": "^0.3.15", 19 | "@backstage/plugin-scaffolder": "^0.9.10", 20 | "@backstage/plugin-search": "^0.4.1", 21 | "@backstage/plugin-tech-radar": "^0.4.2", 22 | "@backstage/plugin-techdocs": "^0.9.8", 23 | "@backstage/plugin-user-settings": "^0.2.12", 24 | "@backstage/test-utils": "^0.1.14", 25 | "@backstage/theme": "^0.2.8", 26 | "@material-ui/core": "^4.11.0", 27 | "@material-ui/icons": "^4.9.1", 28 | "history": "^5.0.0", 29 | "react": "^16.13.1", 30 | "react-dom": "^16.13.1", 31 | "react-router": "6.0.0-beta.0", 32 | "react-router-dom": "6.0.0-beta.0", 33 | "react-use": "^15.3.3" 34 | }, 35 | "devDependencies": { 36 | "@testing-library/jest-dom": "^5.10.1", 37 | "@testing-library/react": "^10.4.1", 38 | "@testing-library/user-event": "^12.0.7", 39 | "@types/jest": "^26.0.7", 40 | "@types/node": "^14.14.32", 41 | "@types/react-dom": "^16.9.8", 42 | "cross-env": "^7.0.0", 43 | "cypress": "^7.3.0", 44 | "eslint-plugin-cypress": "^2.10.3", 45 | "start-server-and-test": "^1.10.11" 46 | }, 47 | "scripts": { 48 | "start": "backstage-cli app:serve", 49 | "build": "backstage-cli app:build", 50 | "test": "backstage-cli test", 51 | "lint": "backstage-cli lint", 52 | "clean": "backstage-cli clean", 53 | "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", 54 | "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", 55 | "cy:dev": "cypress open", 56 | "cy:run": "cypress run" 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | }, 70 | "files": [ 71 | "dist" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /packages/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfabrik/backstage-terraform/f0585f556c21ce7b73850ca94b22ef6f3a0ebe7a/packages/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfabrik/backstage-terraform/f0585f556c21ce7b73850ca94b22ef6f3a0ebe7a/packages/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfabrik/backstage-terraform/f0585f556c21ce7b73850ca94b22ef6f3a0ebe7a/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfabrik/backstage-terraform/f0585f556c21ce7b73850ca94b22ef6f3a0ebe7a/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfabrik/backstage-terraform/f0585f556c21ce7b73850ca94b22ef6f3a0ebe7a/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 21 | 22 | 23 | 28 | 34 | 40 | 45 | 50 | <%= app.title %> 51 | <% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId 52 | === 'string') { %> 53 | 57 | 66 | <% } %> 67 | 68 | 69 | 70 |
71 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Backstage", 3 | "name": "Backstage", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | Created by potrace 1.11, written by Peter Selinger 2001-2013 -------------------------------------------------------------------------------- /packages/app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithEffects } from '@backstage/test-utils'; 3 | import App from './App'; 4 | 5 | describe('App', () => { 6 | it('should render', async () => { 7 | process.env = { 8 | NODE_ENV: 'test', 9 | APP_CONFIG: [ 10 | { 11 | data: { 12 | app: { title: 'Test' }, 13 | backend: { baseUrl: 'http://localhost:7000' }, 14 | techdocs: { 15 | storageUrl: 'http://localhost:7000/api/techdocs/static/docs', 16 | }, 17 | }, 18 | context: 'test', 19 | }, 20 | ] as any, 21 | }; 22 | 23 | const rendered = await renderWithEffects(); 24 | expect(rendered.baseElement).toBeInTheDocument(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, Route } from 'react-router'; 3 | import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; 4 | import { 5 | CatalogEntityPage, 6 | CatalogIndexPage, 7 | catalogPlugin, 8 | } from '@backstage/plugin-catalog'; 9 | import {CatalogImportPage, catalogImportPlugin} from '@backstage/plugin-catalog-import'; 10 | import { 11 | ScaffolderPage, 12 | scaffolderPlugin 13 | } from '@backstage/plugin-scaffolder'; 14 | import { SearchPage } from '@backstage/plugin-search'; 15 | import { TechRadarPage } from '@backstage/plugin-tech-radar'; 16 | import { TechdocsPage } from '@backstage/plugin-techdocs'; 17 | import { UserSettingsPage } from '@backstage/plugin-user-settings'; 18 | import { apis } from './apis'; 19 | import { entityPage } from './components/catalog/EntityPage'; 20 | import { Root } from './components/Root'; 21 | 22 | import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; 23 | import { createApp, FlatRoutes } from '@backstage/core-app-api'; 24 | 25 | const app = createApp({ 26 | apis, 27 | bindRoutes({ bind }) { 28 | bind(catalogPlugin.externalRoutes, { 29 | createComponent: scaffolderPlugin.routes.root, 30 | }); 31 | bind(apiDocsPlugin.externalRoutes, { 32 | createComponent: scaffolderPlugin.routes.root, 33 | }); 34 | bind(scaffolderPlugin.externalRoutes, { 35 | registerComponent: catalogImportPlugin.routes.importPage, 36 | }); 37 | }, 38 | }); 39 | 40 | const AppProvider = app.getProvider(); 41 | const AppRouter = app.getRouter(); 42 | 43 | const routes = ( 44 | 45 | 46 | } /> 47 | } 50 | > 51 | {entityPage} 52 | 53 | } /> 54 | } /> 55 | } /> 56 | } 59 | /> 60 | } /> 61 | } /> 62 | } /> 63 | 64 | ); 65 | 66 | const App = () => ( 67 | 68 | 69 | 70 | 71 | {routes} 72 | 73 | 74 | ); 75 | 76 | export default App; 77 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScmIntegrationsApi, scmIntegrationsApiRef 3 | } from '@backstage/integration-react'; 4 | import { AnyApiFactory, configApiRef, createApiFactory } from '@backstage/core-plugin-api'; 5 | 6 | export const apis: AnyApiFactory[] = [ 7 | createApiFactory({ 8 | api: scmIntegrationsApiRef, 9 | deps: { configApi: configApiRef }, 10 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 11 | }), 12 | ]; 13 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoFull.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { makeStyles } from '@material-ui/core'; 19 | 20 | const useStyles = makeStyles({ 21 | svg: { 22 | width: 'auto', 23 | height: 30, 24 | }, 25 | path: { 26 | fill: '#7df3e1', 27 | }, 28 | }); 29 | const LogoFull = () => { 30 | const classes = useStyles(); 31 | 32 | return ( 33 | 38 | 42 | 43 | ); 44 | }; 45 | 46 | export default LogoFull; 47 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { makeStyles } from '@material-ui/core'; 19 | 20 | const useStyles = makeStyles({ 21 | svg: { 22 | width: 'auto', 23 | height: 28, 24 | }, 25 | path: { 26 | fill: '#7df3e1', 27 | }, 28 | }); 29 | 30 | const LogoIcon = () => { 31 | const classes = useStyles(); 32 | 33 | return ( 34 | 39 | 43 | 44 | ); 45 | }; 46 | 47 | export default LogoIcon; 48 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, { useContext, PropsWithChildren } from 'react'; 18 | import { Link, makeStyles } from '@material-ui/core'; 19 | import HomeIcon from '@material-ui/icons/Home'; 20 | import ExtensionIcon from '@material-ui/icons/Extension'; 21 | import MapIcon from '@material-ui/icons/MyLocation'; 22 | import LibraryBooks from '@material-ui/icons/LibraryBooks'; 23 | import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; 24 | import LogoFull from './LogoFull'; 25 | import LogoIcon from './LogoIcon'; 26 | import { NavLink } from 'react-router-dom'; 27 | import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; 28 | import { SidebarSearch } from '@backstage/plugin-search'; 29 | import { 30 | Sidebar, 31 | SidebarPage, 32 | sidebarConfig, 33 | SidebarContext, 34 | SidebarItem, 35 | SidebarDivider, 36 | SidebarSpace, 37 | } from '@backstage/core-components'; 38 | 39 | const useSidebarLogoStyles = makeStyles({ 40 | root: { 41 | width: sidebarConfig.drawerWidthClosed, 42 | height: 3 * sidebarConfig.logoHeight, 43 | display: 'flex', 44 | flexFlow: 'row nowrap', 45 | alignItems: 'center', 46 | marginBottom: -14, 47 | }, 48 | link: { 49 | width: sidebarConfig.drawerWidthClosed, 50 | marginLeft: 24, 51 | }, 52 | }); 53 | 54 | const SidebarLogo = () => { 55 | const classes = useSidebarLogoStyles(); 56 | const { isOpen } = useContext(SidebarContext); 57 | 58 | return ( 59 |
60 | 66 | {isOpen ? : } 67 | 68 |
69 | ); 70 | }; 71 | 72 | export const Root = ({ children }: PropsWithChildren<{}>) => ( 73 | 74 | 75 | 76 | 77 | 78 | {/* Global nav, not org-specific */} 79 | 80 | 81 | 82 | 83 | {/* End global nav */} 84 | 85 | 86 | 87 | 88 | 89 | 90 | {children} 91 | 92 | ); 93 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { Root } from './Root'; 18 | -------------------------------------------------------------------------------- /packages/app/src/components/catalog/EntityPage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import React from 'react'; 17 | import { Button, Grid } from '@material-ui/core'; 18 | import { 19 | EntityApiDefinitionCard, 20 | EntityConsumedApisCard, 21 | EntityConsumingComponentsCard, 22 | EntityHasApisCard, 23 | EntityProvidedApisCard, 24 | EntityProvidingComponentsCard, 25 | } from '@backstage/plugin-api-docs'; 26 | import { 27 | EntityAboutCard, 28 | EntityDependsOnComponentsCard, 29 | EntityDependsOnResourcesCard, 30 | EntitySystemDiagramCard, 31 | EntityHasComponentsCard, 32 | EntityHasResourcesCard, 33 | EntityHasSubcomponentsCard, 34 | EntityHasSystemsCard, 35 | EntityLayout, 36 | EntityLinksCard, 37 | EntitySwitch, 38 | isComponentType, 39 | isKind, 40 | } from '@backstage/plugin-catalog'; 41 | import { 42 | isGithubActionsAvailable, 43 | EntityGithubActionsContent, 44 | } from '@backstage/plugin-github-actions'; 45 | import { 46 | EntityUserProfileCard, 47 | EntityGroupProfileCard, 48 | EntityMembersListCard, 49 | EntityOwnershipCard, 50 | } from '@backstage/plugin-org'; 51 | import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; 52 | import { EmptyState } from '@backstage/core-components'; 53 | 54 | const cicdContent = ( 55 | // This is an example of how you can implement your company's logic in entity page. 56 | // You can for example enforce that all components of type 'service' should use GitHubActions 57 | 58 | 59 | 60 | 61 | 62 | 63 | 73 | Read more 74 | 75 | } 76 | /> 77 | 78 | 79 | ); 80 | 81 | const overviewContent = ( 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | 95 | const serviceEntityPage = ( 96 | 97 | 98 | {overviewContent} 99 | 100 | 101 | 102 | {cicdContent} 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | ); 132 | 133 | const websiteEntityPage = ( 134 | 135 | 136 | {overviewContent} 137 | 138 | 139 | 140 | {cicdContent} 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | ); 159 | 160 | const defaultEntityPage = ( 161 | 162 | 163 | {overviewContent} 164 | 165 | 166 | 167 | 168 | 169 | 170 | ); 171 | 172 | const componentPage = ( 173 | 174 | 175 | {serviceEntityPage} 176 | 177 | 178 | 179 | {websiteEntityPage} 180 | 181 | 182 | {defaultEntityPage} 183 | 184 | ); 185 | 186 | const apiPage = ( 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | ); 213 | 214 | const userPage = ( 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | ); 228 | 229 | const groupPage = ( 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | ); 246 | 247 | const systemPage = ( 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ); 270 | 271 | const domainPage = ( 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | ); 285 | 286 | export const entityPage = ( 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | {defaultEntityPage} 296 | 297 | ); 298 | -------------------------------------------------------------------------------- /packages/app/src/components/search/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; 3 | 4 | import { CatalogResultListItem } from '@backstage/plugin-catalog'; 5 | import { 6 | SearchBar, 7 | SearchFilter, 8 | SearchResult, 9 | DefaultResultListItem, 10 | } from '@backstage/plugin-search'; 11 | import { Content, Header, Page } from '@backstage/core-components'; 12 | 13 | const useStyles = makeStyles((theme: Theme) => ({ 14 | bar: { 15 | padding: theme.spacing(1, 0), 16 | }, 17 | filters: { 18 | padding: theme.spacing(2), 19 | }, 20 | filter: { 21 | '& + &': { 22 | marginTop: theme.spacing(2.5), 23 | }, 24 | }, 25 | })); 26 | 27 | const SearchPage = () => { 28 | const classes = useStyles(); 29 | 30 | return ( 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 52 | 53 | 54 | 55 | 56 | {({ results }) => ( 57 | 58 | {results.map(({ type, document }) => { 59 | switch (type) { 60 | case 'software-catalog': 61 | return ( 62 | 66 | ); 67 | default: 68 | return ( 69 | 73 | ); 74 | } 75 | })} 76 | 77 | )} 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export const searchPage = ; 87 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@backstage/cli/asset-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /packages/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@backstage/cli/config/eslint.backend')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 - Create yarn install skeleton layer 2 | FROM node:14-buster-slim AS packages 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY package.json yarn.lock ./ 7 | 8 | # COPY plugins 9 | COPY packages packages 10 | 11 | RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf 12 | 13 | # Stage 2 - Install dependencies and build packages 14 | FROM node:14-buster-slim AS build 15 | 16 | WORKDIR /usr/src/app 17 | COPY --from=packages /usr/src/app . 18 | 19 | RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)" 20 | 21 | COPY . . 22 | 23 | COPY app-config.yaml ./ 24 | 25 | RUN yarn tsc 26 | RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies 27 | 28 | # Stage 3 - Build the actual backend image and install production dependencies 29 | FROM node:14-alpine3.13 30 | 31 | ENV NPM_VERSION=6.14.12 32 | ARG NPM_REGISTRY="https://registry.npmjs.org" 33 | 34 | WORKDIR /usr/src/app 35 | 36 | # Fix openjdk-11-jdk-headless error 37 | RUN mkdir -p /usr/share/man/man1 38 | 39 | # Install cookiecutter 40 | RUN apk add --no-cache python3 python3-dev py3-pip py3-matplotlib py3-wheel g++ \ 41 | gcc musl-dev openjdk11-jre-headless curl graphviz ttf-dejavu fontconfig 42 | 43 | # Download plantuml file, Validate checksum & Move plantuml file 44 | RUN curl -o plantuml.jar -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2021.4.jar/download \ 45 | && echo "be498123d20eaea95a94b174d770ef94adfdca18 plantuml.jar" | sha1sum -c - && mv plantuml.jar /opt/plantuml.jar 46 | 47 | # Install cookiecutter and mkdocs 48 | RUN pip3 install cookiecutter && pip3 install mkdocs-techdocs-core==0.0.16 49 | 50 | RUN apk del curl 51 | 52 | # Create script to call plantuml.jar from a location in path 53 | RUN echo $'#!/bin/sh\n\njava -jar '/opt/plantuml.jar' ${@}' >> /usr/local/bin/plantuml 54 | RUN chmod 755 /usr/local/bin/plantuml 55 | 56 | # Install dependencies and update npm 57 | RUN npm config set registry ${NPM_REGISTRY} \ 58 | && npm config set strict-ssl false \ 59 | && yarn config set registry ${NPM_REGISTRY} \ 60 | && yarn config set strict-ssl false \ 61 | && npm install -g npm@${NPM_VERSION} 62 | 63 | # Copy from build stage 64 | COPY --from=build /usr/src/app/yarn.lock /usr/src/app/package.json /usr/src/app/packages/backend/dist/skeleton.tar.gz ./ 65 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 66 | 67 | RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" 68 | 69 | COPY --from=build /usr/src/app/packages/backend/dist/bundle.tar.gz . 70 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 71 | 72 | COPY app-config.yaml ./ 73 | 74 | ENV PORT 7000 75 | 76 | CMD ["node", "packages/backend", "--config", "app-config.yaml"] 77 | -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # example-backend 2 | 3 | This package is an EXAMPLE of a Backstage backend. 4 | 5 | The main purpose of this package is to provide a test bed for Backstage plugins 6 | that have a backend part. Feel free to experiment locally or within your fork by 7 | adding dependencies and routes to this backend, to try things out. 8 | 9 | Our goal is to eventually amend the create-app flow of the CLI, such that a 10 | production ready version of a backend skeleton is made alongside the frontend 11 | app. Until then, feel free to experiment here! 12 | 13 | ## Development 14 | 15 | To run the example backend, first go to the project root and run 16 | 17 | ```bash 18 | yarn install 19 | yarn tsc 20 | yarn build 21 | ``` 22 | 23 | You should only need to do this once. 24 | 25 | After that, go to the `packages/backend` directory and run 26 | 27 | ```bash 28 | AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ 29 | AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ 30 | AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ 31 | AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ 32 | LOG_LEVEL=debug \ 33 | yarn start 34 | ``` 35 | 36 | Substitute `x` for actual values, or leave them as dummy values just to try out 37 | the backend without using the auth or sentry features. 38 | 39 | The backend starts up on port 7000 per default. 40 | 41 | ## Populating The Catalog 42 | 43 | If you want to use the catalog functionality, you need to add so called 44 | locations to the backend. These are places where the backend can find some 45 | entity descriptor data to consume and serve. For more information, see 46 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog). 47 | 48 | To get started quickly, this template already includes some statically configured example locations 49 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 50 | like, and also override them for local development in `app-config.local.yaml`. 51 | 52 | ## Authentication 53 | 54 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 55 | to its comprehensive set of supported authentication 56 | [strategies](http://www.passportjs.org/packages/). 57 | 58 | Read more about the 59 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 60 | and 61 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 62 | 63 | ## Documentation 64 | 65 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 66 | - [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) 67 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "main": "dist/index.cjs.js", 5 | "types": "src/index.ts", 6 | "private": true, 7 | "engines": { 8 | "node": "12 || 14" 9 | }, 10 | "scripts": { 11 | "build": "backstage-cli backend:bundle", 12 | "build-image": "docker build ../.. -f Dockerfile --tag backstage", 13 | "start": "backstage-cli backend:dev", 14 | "lint": "backstage-cli lint", 15 | "test": "backstage-cli test", 16 | "clean": "backstage-cli clean", 17 | "migrate:create": "knex migrate:make -x ts" 18 | }, 19 | "dependencies": { 20 | "app": "0.0.0", 21 | "@backstage/backend-common": "^0.8.4", 22 | "@backstage/catalog-model": "^0.8.4", 23 | "@backstage/catalog-client": "^0.3.15", 24 | "@backstage/config": "^0.1.5", 25 | "@backstage/plugin-app-backend": "^0.3.14", 26 | "@backstage/plugin-auth-backend": "^0.3.15", 27 | "@backstage/plugin-catalog-backend": "^0.11.0", 28 | "@backstage/plugin-proxy-backend": "^0.2.11", 29 | "@backstage/plugin-scaffolder-backend": "^0.12.4", 30 | "@backstage/plugin-search-backend": "^0.2.1", 31 | "@backstage/plugin-search-backend-node": "^0.2.2", 32 | "@backstage/plugin-techdocs-backend": "^0.8.5", 33 | "@gitbeaker/node": "^30.2.0", 34 | "@octokit/rest": "^18.5.3", 35 | "dockerode": "^3.2.1", 36 | "express": "^4.17.1", 37 | "express-promise-router": "^4.1.0", 38 | "knex": "^0.21.6", 39 | "pg": "^8.3.0", 40 | "winston": "^3.2.1" 41 | }, 42 | "devDependencies": { 43 | "@backstage/cli": "^0.7.3", 44 | "@types/dockerode": "^3.2.1", 45 | "@types/express": "^4.17.6", 46 | "@types/express-serve-static-core": "^4.17.5" 47 | }, 48 | "files": [ 49 | "dist" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/backend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { PluginEnvironment } from './types'; 2 | 3 | describe('test', () => { 4 | it('unbreaks the test runner', () => { 5 | const unbreaker = {} as PluginEnvironment; 6 | expect(unbreaker).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Hi! 3 | * 4 | * Note that this is an EXAMPLE Backstage backend. Please check the README. 5 | * 6 | * Happy hacking! 7 | */ 8 | 9 | import Router from 'express-promise-router'; 10 | import { 11 | createServiceBuilder, 12 | loadBackendConfig, 13 | getRootLogger, 14 | useHotMemoize, 15 | notFoundHandler, 16 | CacheManager, 17 | DatabaseManager, 18 | SingleHostDiscovery, 19 | UrlReaders, 20 | } from '@backstage/backend-common'; 21 | import { Config } from '@backstage/config'; 22 | import app from './plugins/app'; 23 | import auth from './plugins/auth'; 24 | import catalog from './plugins/catalog'; 25 | import scaffolder from './plugins/scaffolder'; 26 | import proxy from './plugins/proxy'; 27 | import techdocs from './plugins/techdocs'; 28 | import search from './plugins/search'; 29 | import { PluginEnvironment } from './types'; 30 | 31 | function makeCreateEnv(config: Config) { 32 | const root = getRootLogger(); 33 | const reader = UrlReaders.default({ logger: root, config }); 34 | const discovery = SingleHostDiscovery.fromConfig(config); 35 | 36 | root.info(`Created UrlReader ${reader}`); 37 | 38 | const cacheManager = CacheManager.fromConfig(config); 39 | const databaseManager = DatabaseManager.fromConfig(config); 40 | 41 | return (plugin: string): PluginEnvironment => { 42 | const logger = root.child({ type: 'plugin', plugin }); 43 | const database = databaseManager.forPlugin(plugin); 44 | const cache = cacheManager.forPlugin(plugin); 45 | return { logger, database, cache, config, reader, discovery }; 46 | }; 47 | } 48 | 49 | async function main() { 50 | const config = await loadBackendConfig({ 51 | argv: process.argv, 52 | logger: getRootLogger(), 53 | }); 54 | const createEnv = makeCreateEnv(config); 55 | 56 | const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); 57 | const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); 58 | const authEnv = useHotMemoize(module, () => createEnv('auth')); 59 | const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); 60 | const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); 61 | const searchEnv = useHotMemoize(module, () => createEnv('search')); 62 | const appEnv = useHotMemoize(module, () => createEnv('app')); 63 | 64 | const apiRouter = Router(); 65 | apiRouter.use('/catalog', await catalog(catalogEnv)); 66 | apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); 67 | apiRouter.use('/auth', await auth(authEnv)); 68 | apiRouter.use('/techdocs', await techdocs(techdocsEnv)); 69 | apiRouter.use('/proxy', await proxy(proxyEnv)); 70 | apiRouter.use('/search', await search(searchEnv)); 71 | apiRouter.use(notFoundHandler()); 72 | 73 | const service = createServiceBuilder(module) 74 | .loadConfig(config) 75 | .addRouter('/api', apiRouter) 76 | .addRouter('', await app(appEnv)); 77 | 78 | await service.start().catch(err => { 79 | console.log(err); 80 | process.exit(1); 81 | }); 82 | } 83 | 84 | module.hot?.accept(); 85 | main().catch(error => { 86 | console.error(`Backend failed to start up, ${error}`); 87 | process.exit(1); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/app.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-app-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin({ 6 | logger, 7 | config, 8 | }: PluginEnvironment): Promise { 9 | return await createRouter({ 10 | logger, 11 | config, 12 | appPackageName: 'app', 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-auth-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin({ 6 | logger, 7 | database, 8 | config, 9 | discovery, 10 | }: PluginEnvironment): Promise { 11 | return await createRouter({ logger, config, database, discovery }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/catalog.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CatalogBuilder, 3 | createRouter 4 | } from '@backstage/plugin-catalog-backend'; 5 | import { Router } from 'express'; 6 | import { PluginEnvironment } from '../types'; 7 | 8 | export default async function createPlugin(env: PluginEnvironment): Promise { 9 | const builder = await CatalogBuilder.create(env); 10 | const { 11 | entitiesCatalog, 12 | locationsCatalog, 13 | locationService, 14 | processingEngine, 15 | locationAnalyzer, 16 | } = await builder.build(); 17 | 18 | await processingEngine.start(); 19 | 20 | return await createRouter({ 21 | entitiesCatalog, 22 | locationsCatalog, 23 | locationService, 24 | locationAnalyzer, 25 | logger: env.logger, 26 | config: env.config, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/proxy.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-proxy-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin({ 6 | logger, 7 | config, 8 | discovery, 9 | }: PluginEnvironment): Promise { 10 | return await createRouter({ logger, config, discovery }); 11 | } 12 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/scaffolder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DockerContainerRunner, 3 | SingleHostDiscovery, 4 | } from '@backstage/backend-common'; 5 | import { CatalogClient } from '@backstage/catalog-client'; 6 | import { 7 | CookieCutter, 8 | CreateReactAppTemplater, 9 | createRouter, 10 | Preparers, 11 | Publishers, 12 | Templaters, 13 | } from '@backstage/plugin-scaffolder-backend'; 14 | import Docker from 'dockerode'; 15 | import { Router } from 'express'; 16 | import type { PluginEnvironment } from '../types'; 17 | 18 | export default async function createPlugin({ 19 | logger, 20 | config, 21 | database, 22 | reader, 23 | }: PluginEnvironment): Promise { 24 | const dockerClient = new Docker(); 25 | const containerRunner = new DockerContainerRunner({ dockerClient }); 26 | 27 | const cookiecutterTemplater = new CookieCutter({ containerRunner }); 28 | const craTemplater = new CreateReactAppTemplater({ containerRunner }); 29 | const templaters = new Templaters(); 30 | 31 | templaters.register('cookiecutter', cookiecutterTemplater); 32 | templaters.register('cra', craTemplater); 33 | 34 | const preparers = await Preparers.fromConfig(config, { logger }); 35 | const publishers = await Publishers.fromConfig(config, { logger }); 36 | 37 | const discovery = SingleHostDiscovery.fromConfig(config); 38 | const catalogClient = new CatalogClient({ discoveryApi: discovery }); 39 | 40 | return await createRouter({ 41 | preparers, 42 | templaters, 43 | publishers, 44 | logger, 45 | config, 46 | database, 47 | catalogClient, 48 | reader, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/search.ts: -------------------------------------------------------------------------------- 1 | import { useHotCleanup } from '@backstage/backend-common'; 2 | import { createRouter } from '@backstage/plugin-search-backend'; 3 | import { 4 | IndexBuilder, 5 | LunrSearchEngine, 6 | } from '@backstage/plugin-search-backend-node'; 7 | import { PluginEnvironment } from '../types'; 8 | import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; 9 | 10 | export default async function createPlugin({ 11 | logger, 12 | discovery, 13 | }: PluginEnvironment) { 14 | // Initialize a connection to a search engine. 15 | const searchEngine = new LunrSearchEngine({ logger }); 16 | const indexBuilder = new IndexBuilder({ logger, searchEngine }); 17 | 18 | // Collators are responsible for gathering documents known to plugins. This 19 | // particular collator gathers entities from the software catalog. 20 | indexBuilder.addCollator({ 21 | defaultRefreshIntervalSeconds: 600, 22 | collator: new DefaultCatalogCollator({ discovery }), 23 | }); 24 | 25 | // The scheduler controls when documents are gathered from collators and sent 26 | // to the search engine for indexing. 27 | const { scheduler } = await indexBuilder.build(); 28 | 29 | // A 3 second delay gives the backend server a chance to initialize before 30 | // any collators are executed, which may attempt requests against the API. 31 | setTimeout(() => scheduler.start(), 3000); 32 | useHotCleanup(module, () => scheduler.stop()); 33 | 34 | return await createRouter({ 35 | engine: indexBuilder.getSearchEngine(), 36 | logger, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/techdocs.ts: -------------------------------------------------------------------------------- 1 | import { DockerContainerRunner } from '@backstage/backend-common'; 2 | import { 3 | createRouter, 4 | Generators, 5 | Preparers, 6 | Publisher, 7 | } from '@backstage/plugin-techdocs-backend'; 8 | import Docker from 'dockerode'; 9 | import { Router } from 'express'; 10 | import { PluginEnvironment } from '../types'; 11 | 12 | export default async function createPlugin({ 13 | logger, 14 | config, 15 | discovery, 16 | reader, 17 | }: PluginEnvironment): Promise { 18 | // Preparers are responsible for fetching source files for documentation. 19 | const preparers = await Preparers.fromConfig(config, { 20 | logger, 21 | reader, 22 | }); 23 | 24 | // Docker client (conditionally) used by the generators, based on techdocs.generators config. 25 | const dockerClient = new Docker(); 26 | const containerRunner = new DockerContainerRunner({ dockerClient }); 27 | 28 | // Generators are used for generating documentation sites. 29 | const generators = await Generators.fromConfig(config, { 30 | logger, 31 | containerRunner, 32 | }); 33 | 34 | // Publisher is used for 35 | // 1. Publishing generated files to storage 36 | // 2. Fetching files from storage and passing them to TechDocs frontend. 37 | const publisher = await Publisher.fromConfig(config, { 38 | logger, 39 | discovery, 40 | }); 41 | 42 | // checks if the publisher is working and logs the result 43 | await publisher.getReadiness(); 44 | 45 | return await createRouter({ 46 | preparers, 47 | generators, 48 | publisher, 49 | logger, 50 | config, 51 | discovery, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'winston'; 2 | import { Config } from '@backstage/config'; 3 | import { 4 | PluginCacheManager, 5 | PluginDatabaseManager, 6 | PluginEndpointDiscovery, 7 | UrlReader, 8 | } from '@backstage/backend-common'; 9 | 10 | export type PluginEnvironment = { 11 | logger: Logger; 12 | database: PluginDatabaseManager; 13 | cache: PluginCacheManager; 14 | config: Config; 15 | reader: UrlReader 16 | discovery: PluginEndpointDiscovery; 17 | }; 18 | -------------------------------------------------------------------------------- /terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.44.0" 6 | constraints = "3.44.0" 7 | hashes = [ 8 | "h1:hxQ8n9SHHfAIXd/FtfAqxokFYWBedzZf7xqQZWJajUs=", 9 | "zh:0680315b29a140e9b7e4f5aeed3f2445abdfab31fc9237f34dcad06de4f410df", 10 | "zh:13811322a205fb4a0ee617f0ae51ec94176befdf569235d0c7064db911f0acc7", 11 | "zh:25e427a1cfcb1d411bc12040cf0684158d094416ecf18889a41196bacc761729", 12 | "zh:40cd6acd24b060823f8d116355d8f844461a11925796b1757eb2ee18abc0bc7c", 13 | "zh:94e2463eef555c388cd27f6e85ad803692d6d80ffa621bdc382ab119001d4de4", 14 | "zh:aadc3bc216b14839e85b463f07b8507920ace5f202a608e4a835df23711c8a0d", 15 | "zh:ab50dc1242af5a8fcdb18cf89beeaf2b2146b51ecfcecdbea033913a5f4c1c14", 16 | "zh:ad48bbf4af66b5d48ca07c5c558d2f5724311db4dd943c1c98a7f3f107e03311", 17 | "zh:ad76796c2145a7aaec1970a5244f5c0a9d200556121e2c5b382f296597b1a03c", 18 | "zh:cf0a2181356598f8a2abfeaf0cdf385bdeea7f2e52821c850a2a08b60c26b9f6", 19 | "zh:f76801af6bc34fe4a5bf1c63fa0204e24b81691049efecd6baa1526593f03935", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /terraform/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hashicorp/terraform:1.0.2 2 | RUN apk add --no-cache aws-cli curl openssl bash-completion \ 3 | && curl -L https://amazon-eks.s3.us-west-2.amazonaws.com/1.19.6/2021-01-05/bin/linux/amd64/aws-iam-authenticator > /usr/local/bin/aws-iam-authenticator \ 4 | && chmod +x /usr/local/bin/aws-iam-authenticator \ 5 | && printf "# Shell completion\nsource '/usr/share/bash-completion/bash_completion'" > $HOME/.bashrc \ 6 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | ### VPC 4 | module "aws_vpc" { 5 | source = "./modules/vpc" 6 | project = var.project 7 | vpc_cidr_block = var.vpc_cidr_block 8 | public_subnets = var.public_subnets 9 | } 10 | 11 | ### ALB 12 | module "aws_alb" { 13 | source = "./modules/alb" 14 | project = var.project 15 | vpc_id = module.aws_vpc.vpc_id 16 | subnet_ids = values(module.aws_vpc.public_subnets) 17 | depends_on = [module.aws_vpc] 18 | } 19 | 20 | ### RDS 21 | module "aws_rds" { 22 | source = "./modules/rds" 23 | project = var.project 24 | storage = 20 25 | username = var.postgres_user 26 | password = var.postgres_password 27 | subnet_ids = values(module.aws_vpc.public_subnets) 28 | vpc_id = module.aws_vpc.vpc_id 29 | default_security_group_id = module.aws_alb.default_security_group_id 30 | depends_on = [module.aws_vpc, module.aws_alb] 31 | } 32 | 33 | ### S3 34 | module "aws_s3" { 35 | source = "./modules/s3" 36 | project = var.project 37 | name = "${var.project}-techdocs-demo-${data.aws_caller_identity.current.account_id}" 38 | acl = "private" 39 | versioning = false 40 | } 41 | 42 | ### SSM 43 | module "aws_ssm" { 44 | source = "./modules/ssm" 45 | project = var.project 46 | postgres_host = module.aws_rds.rds_instance_endpoint 47 | postgres_user = var.postgres_user 48 | postgres_password = var.postgres_password 49 | github_token = var.github_token 50 | github_client_id = var.github_client_id 51 | github_client_secret = var.github_client_secret 52 | access_key_id = var.access_key_id 53 | secret_access_key = var.secret_access_key 54 | depends_on = [module.aws_rds] 55 | } 56 | 57 | ### IAM 58 | module "aws_iam" { 59 | source = "./modules/iam" 60 | project = var.project 61 | } 62 | 63 | ### ECR 64 | resource "aws_ecr_repository" "registry" { 65 | name = "${var.project}-image" 66 | image_tag_mutability = "MUTABLE" 67 | image_scanning_configuration { 68 | scan_on_push = false 69 | } 70 | } 71 | 72 | ### ECS 73 | module "aws_ecs" { 74 | source = "./modules/ecs" 75 | project = var.project 76 | docker_image_url = aws_ecr_repository.registry.repository_url 77 | docker_image_tag = var.docker_image_tag 78 | default_region = var.default_region 79 | vpc_id = module.aws_vpc.vpc_id 80 | security_group_ids = [module.aws_alb.default_security_group_id] 81 | subnet_ids = values(module.aws_vpc.public_subnets) 82 | execution_role_arn = module.aws_iam.ecs_role_arn 83 | postgres_host_arn = module.aws_ssm.postgres_host_arn 84 | postgres_user_arn = module.aws_ssm.postgres_user_arn 85 | postgres_password_arn = module.aws_ssm.postgres_password_arn 86 | github_token_arn = module.aws_ssm.github_token_arn 87 | github_client_id_arn = module.aws_ssm.github_client_id_arn 88 | github_client_secret_arn = module.aws_ssm.github_client_secret_arn 89 | tech_docs_bucket_name = module.aws_s3.tech_docs_bucket_name 90 | access_key_id_arn = module.aws_ssm.access_key_id_arn 91 | secret_access_key_arn = module.aws_ssm.secret_access_key_arn 92 | target_group_arn = module.aws_alb.target_group_arn 93 | alb_dns_name = module.aws_alb.alb_dns_name 94 | depends_on = [module.aws_vpc, module.aws_alb, module.aws_s3, module.aws_ssm, module.aws_iam] 95 | } 96 | -------------------------------------------------------------------------------- /terraform/modules/alb/alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "default_sg" { 2 | name = "${var.project}-sg" 3 | description = "Allow traffic to application" 4 | vpc_id = var.vpc_id 5 | 6 | ingress { 7 | from_port = 0 8 | to_port = 0 9 | protocol = -1 10 | self = true 11 | } 12 | 13 | ingress { 14 | description = "Allow HTTP traffic to application" 15 | from_port = 80 16 | to_port = 80 17 | protocol = "tcp" 18 | cidr_blocks = ["0.0.0.0/0"] 19 | } 20 | 21 | egress { 22 | from_port = 0 23 | to_port = 0 24 | protocol = -1 25 | cidr_blocks = ["0.0.0.0/0"] 26 | } 27 | } 28 | 29 | resource "aws_alb" "default_alb" { 30 | name = "${var.project}-alb" 31 | internal = false 32 | load_balancer_type = "application" 33 | security_groups = [aws_security_group.default_sg.id] 34 | subnets = var.subnet_ids 35 | enable_deletion_protection = false 36 | } 37 | 38 | resource "aws_alb_target_group" "default_tg" { 39 | name = "${var.project}-tg" 40 | port = 7000 41 | protocol = "HTTP" 42 | vpc_id = var.vpc_id 43 | target_type = "ip" 44 | 45 | health_check { 46 | healthy_threshold = "2" 47 | interval = "300" 48 | protocol = "HTTP" 49 | matcher = "200" 50 | timeout = "5" 51 | path = "/" 52 | unhealthy_threshold = "2" 53 | } 54 | } 55 | 56 | resource "aws_alb_listener" "default_listener" { 57 | load_balancer_arn = aws_alb.default_alb.arn 58 | protocol = "HTTP" 59 | port = 80 60 | 61 | default_action { 62 | type = "forward" 63 | target_group_arn = aws_alb_target_group.default_tg.id 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /terraform/modules/alb/output.tf: -------------------------------------------------------------------------------- 1 | output default_security_group_id { 2 | value = aws_security_group.default_sg.id 3 | } 4 | 5 | output target_group_arn { 6 | value = aws_alb_target_group.default_tg.arn 7 | } 8 | 9 | output alb_dns_name { 10 | value = aws_alb.default_alb.dns_name 11 | } 12 | -------------------------------------------------------------------------------- /terraform/modules/alb/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable vpc_id { 6 | type = string 7 | } 8 | 9 | variable subnet_ids { 10 | type = list(string) 11 | } 12 | -------------------------------------------------------------------------------- /terraform/modules/ecs/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "default_cluster" { 2 | name = "${var.project}-cluster" 3 | setting { 4 | name = "containerInsights" 5 | value = "enabled" 6 | } 7 | } 8 | 9 | resource "aws_cloudwatch_log_group" "default_log_group" { 10 | name = "/ecs/${var.project}" 11 | } 12 | 13 | resource "aws_ecs_task_definition" "default_task" { 14 | family = "${var.project}-task" 15 | network_mode = "awsvpc" 16 | requires_compatibilities = ["FARGATE"] 17 | cpu = 512 18 | memory = 1024 19 | execution_role_arn = var.execution_role_arn 20 | container_definitions = jsonencode([{ 21 | name = "${var.project}-container" 22 | image = "${var.docker_image_url}:${var.docker_image_tag}" 23 | essential = true 24 | secrets: [ 25 | {"name": "POSTGRES_HOST", "valueFrom": var.postgres_host_arn}, 26 | {"name": "POSTGRES_USER", "valueFrom": var.postgres_user_arn}, 27 | {"name": "POSTGRES_PASSWORD", "valueFrom": var.postgres_password_arn}, 28 | {"name": "GITHUB_TOKEN", "valueFrom": var.github_token_arn}, 29 | {"name": "AUTH_GITHUB_CLIENT_ID", "valueFrom": var.github_client_id_arn}, 30 | {"name": "AUTH_GITHUB_CLIENT_SECRET", "valueFrom": var.github_client_secret_arn}, 31 | {"name": "ACCESS_KEY_ID", "valueFrom": var.access_key_id_arn}, 32 | {"name": "SECRET_ACCESS_KEY", "valueFrom": var.secret_access_key_arn} 33 | ] 34 | environment: [ 35 | {"name": "APP_DOMAIN", "value": "http://${var.alb_dns_name}"}, 36 | {"name": "APP_URL", "value": "http://${var.alb_dns_name}"}, 37 | {"name": "BACKEND_URL", "value": "http://${var.alb_dns_name}"}, 38 | {"name": "POSTGRES_PORT", "value": "5432"}, 39 | {"name": "DEFAULT_REGION", "value": var.default_region}, 40 | {"name": "BUCKET_NAME", "value": var.tech_docs_bucket_name} 41 | ], 42 | logConfiguration = { 43 | logDriver = "awslogs" 44 | options: { 45 | "awslogs-group": "/ecs/${var.project}", 46 | "awslogs-region": var.default_region, 47 | "awslogs-stream-prefix": "ecs" 48 | } 49 | } 50 | portMappings = [{ 51 | protocol = "tcp" 52 | containerPort = 7000 53 | hostPort = 7000 54 | }] 55 | }]) 56 | tags = { 57 | Name = "${var.project}-task" 58 | } 59 | } 60 | 61 | resource "aws_ecs_service" "default_service" { 62 | name = "${var.project}-service" 63 | cluster = aws_ecs_cluster.default_cluster.id 64 | task_definition = aws_ecs_task_definition.default_task.arn 65 | launch_type = "FARGATE" 66 | platform_version = "1.4.0" 67 | desired_count = 1 68 | deployment_minimum_healthy_percent = 100 69 | deployment_maximum_percent = 200 70 | scheduling_strategy = "REPLICA" 71 | depends_on = [aws_ecs_task_definition.default_task] 72 | 73 | network_configuration { 74 | security_groups = var.security_group_ids 75 | subnets = var.subnet_ids 76 | assign_public_ip = true 77 | } 78 | 79 | load_balancer { 80 | target_group_arn = var.target_group_arn 81 | container_name = "${var.project}-container" 82 | container_port = 7000 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /terraform/modules/ecs/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable docker_image_url { 6 | type = string 7 | } 8 | 9 | variable docker_image_tag { 10 | type = string 11 | } 12 | 13 | variable default_region { 14 | type = string 15 | } 16 | 17 | variable vpc_id { 18 | type = string 19 | } 20 | 21 | variable security_group_ids { 22 | type = list(string) 23 | } 24 | 25 | variable subnet_ids { 26 | type = list(string) 27 | } 28 | 29 | variable execution_role_arn { 30 | type = string 31 | } 32 | 33 | variable postgres_host_arn { 34 | type = string 35 | } 36 | 37 | variable postgres_user_arn { 38 | type = string 39 | } 40 | 41 | variable postgres_password_arn { 42 | type = string 43 | } 44 | 45 | variable github_token_arn { 46 | type = string 47 | } 48 | 49 | variable github_client_id_arn { 50 | type = string 51 | } 52 | 53 | variable github_client_secret_arn { 54 | type = string 55 | } 56 | 57 | variable tech_docs_bucket_name { 58 | type = string 59 | } 60 | 61 | variable access_key_id_arn { 62 | type = string 63 | } 64 | 65 | variable secret_access_key_arn { 66 | type = string 67 | } 68 | 69 | variable target_group_arn { 70 | type = string 71 | } 72 | 73 | variable alb_dns_name { 74 | type = string 75 | } 76 | -------------------------------------------------------------------------------- /terraform/modules/iam/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_policy" "ssm_policy" { 2 | name = "${var.project}-ssm-policy" 3 | path = "/" 4 | description = "Access to Parameter Store variables" 5 | policy = jsonencode({ 6 | "Version": "2012-10-17", 7 | "Statement": [ 8 | { 9 | "Effect": "Allow", 10 | "Action": [ 11 | "ssm:GetParameters" 12 | ], 13 | "Resource": "*" 14 | } 15 | ] 16 | }) 17 | } 18 | 19 | resource "aws_iam_policy" "logs_policy" { 20 | name = "${var.project}-logs-policy" 21 | path = "/" 22 | description = "Access to Cloudwatch" 23 | policy = jsonencode({ 24 | "Version": "2012-10-17", 25 | "Statement": [ 26 | { 27 | "Effect": "Allow", 28 | "Action": [ 29 | "logs:CreateLogGroup", 30 | "logs:CreateLogStream", 31 | "logs:PutLogEvents", 32 | "logs:DescribeLogStreams" 33 | ], 34 | "Resource": [ 35 | "arn:aws:logs:*" 36 | ] 37 | } 38 | ] 39 | }) 40 | } 41 | 42 | resource "aws_iam_policy" "ecr_policy" { 43 | name = "${var.project}-ecr-policy" 44 | path = "/" 45 | description = "Access to ECR" 46 | policy = jsonencode({ 47 | "Version": "2012-10-17", 48 | "Statement": [ 49 | { 50 | "Effect": "Allow", 51 | "Action": [ 52 | "ecr:BatchCheckLayerAvailability", 53 | "ecr:BatchGetImage", 54 | "ecr:GetDownloadUrlForLayer", 55 | "ecr:GetAuthorizationToken" 56 | ], 57 | "Resource": "*" 58 | } 59 | ] 60 | }) 61 | } 62 | 63 | data "aws_iam_policy_document" "ecs_tasks_execution_role" { 64 | statement { 65 | actions = ["sts:AssumeRole"] 66 | principals { 67 | type = "Service" 68 | identifiers = ["ecs-tasks.amazonaws.com"] 69 | } 70 | } 71 | } 72 | 73 | resource "aws_iam_role" "backstage_role" { 74 | name = "${var.project}-role" 75 | assume_role_policy = data.aws_iam_policy_document.ecs_tasks_execution_role.json 76 | } 77 | 78 | resource "aws_iam_role_policy_attachment" "backstage-ssm-policy-attach" { 79 | role = aws_iam_role.backstage_role.name 80 | policy_arn = aws_iam_policy.ssm_policy.arn 81 | } 82 | 83 | resource "aws_iam_role_policy_attachment" "backstage-logs-policy-attach" { 84 | role = aws_iam_role.backstage_role.name 85 | policy_arn = aws_iam_policy.logs_policy.arn 86 | } 87 | 88 | resource "aws_iam_role_policy_attachment" "backstage-ecr-policy-attach" { 89 | role = aws_iam_role.backstage_role.name 90 | policy_arn = aws_iam_policy.ecr_policy.arn 91 | } 92 | -------------------------------------------------------------------------------- /terraform/modules/iam/output.tf: -------------------------------------------------------------------------------- 1 | output ecs_role_arn { 2 | value = aws_iam_role.backstage_role.arn 3 | } 4 | -------------------------------------------------------------------------------- /terraform/modules/iam/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | -------------------------------------------------------------------------------- /terraform/modules/rds/output.tf: -------------------------------------------------------------------------------- 1 | output rds_instance_endpoint { 2 | value = aws_db_instance.default_db.address 3 | } 4 | -------------------------------------------------------------------------------- /terraform/modules/rds/rds.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "rds_instance_sg" { 2 | name = "${var.project}-rds-sg" 3 | description = "Allow traffic to DB from default security group" 4 | vpc_id = var.vpc_id 5 | ingress { 6 | description = "Connection to DB" 7 | from_port = 5432 8 | to_port = 5432 9 | protocol = "tcp" 10 | security_groups = [var.default_security_group_id] 11 | } 12 | } 13 | 14 | resource "aws_db_subnet_group" "default_sn" { 15 | name = "rds_subnet_group" 16 | subnet_ids = var.subnet_ids 17 | } 18 | 19 | resource "aws_db_instance" "default_db" { 20 | identifier = var.project 21 | name = "backstagedb" 22 | allocated_storage = var.storage 23 | storage_type = "gp2" 24 | engine = "postgres" 25 | engine_version = "13.2" 26 | parameter_group_name = "default.postgres13" 27 | instance_class = "db.t3.micro" 28 | username = var.username 29 | password = var.password 30 | multi_az = false 31 | skip_final_snapshot = true 32 | deletion_protection = false 33 | backup_retention_period = 15 34 | backup_window = "03:00-04:00" 35 | maintenance_window = "wed:04:30-wed:05:30" 36 | availability_zone = "eu-west-1b" 37 | db_subnet_group_name = aws_db_subnet_group.default_sn.name 38 | vpc_security_group_ids = [aws_security_group.rds_instance_sg.id] 39 | } 40 | -------------------------------------------------------------------------------- /terraform/modules/rds/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable storage { 6 | type = number 7 | } 8 | 9 | variable username { 10 | type = string 11 | } 12 | 13 | variable password { 14 | type = string 15 | } 16 | 17 | variable vpc_id { 18 | type = string 19 | } 20 | 21 | variable subnet_ids { 22 | type = list(string) 23 | } 24 | 25 | variable default_security_group_id { 26 | type = string 27 | } 28 | -------------------------------------------------------------------------------- /terraform/modules/s3/output.tf: -------------------------------------------------------------------------------- 1 | output tech_docs_bucket_name { 2 | value = aws_s3_bucket.tech_docs_bucket.id 3 | } 4 | -------------------------------------------------------------------------------- /terraform/modules/s3/s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "tech_docs_bucket" { 2 | bucket = var.name 3 | acl = var.acl 4 | versioning { 5 | enabled = var.versioning 6 | } 7 | tags = { 8 | Name = var.name 9 | } 10 | } 11 | 12 | resource "aws_s3_bucket_public_access_block" "tech_docs_bucket_acl" { 13 | bucket = var.name 14 | block_public_acls = true 15 | block_public_policy = true 16 | ignore_public_acls = true 17 | restrict_public_buckets = true 18 | depends_on = [ aws_s3_bucket.tech_docs_bucket ] 19 | } 20 | -------------------------------------------------------------------------------- /terraform/modules/s3/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable name { 6 | type = string 7 | } 8 | 9 | variable acl { 10 | type = string 11 | } 12 | 13 | variable versioning { 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /terraform/modules/ssm/output.tf: -------------------------------------------------------------------------------- 1 | output postgres_host_arn { 2 | value = aws_ssm_parameter.postgres_host.arn 3 | } 4 | 5 | output postgres_user_arn { 6 | value = aws_ssm_parameter.postgres_user.arn 7 | } 8 | 9 | output postgres_password_arn { 10 | value = aws_ssm_parameter.postgres_password.arn 11 | } 12 | 13 | output github_token_arn { 14 | value = aws_ssm_parameter.github_token.arn 15 | } 16 | 17 | output github_client_id_arn { 18 | value = aws_ssm_parameter.github_client_id.arn 19 | } 20 | 21 | output github_client_secret_arn { 22 | value = aws_ssm_parameter.github_client_secret.arn 23 | } 24 | 25 | output access_key_id_arn { 26 | value = aws_ssm_parameter.access_key_id.arn 27 | } 28 | 29 | output secret_access_key_arn { 30 | value = aws_ssm_parameter.secret_access_key.arn 31 | } 32 | -------------------------------------------------------------------------------- /terraform/modules/ssm/ssm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "postgres_host" { 2 | name = "POSTGRES_HOST" 3 | type = "String" 4 | value = var.postgres_host 5 | } 6 | 7 | resource "aws_ssm_parameter" "postgres_user" { 8 | name = "POSTGRES_USER" 9 | type = "String" 10 | value = var.postgres_user 11 | } 12 | 13 | resource "aws_ssm_parameter" "postgres_password" { 14 | name = "POSTGRES_PASSWORD" 15 | type = "String" 16 | value = var.postgres_password 17 | } 18 | 19 | resource "aws_ssm_parameter" "github_token" { 20 | name = "GITHUB_TOKEN" 21 | type = "String" 22 | value = var.github_token 23 | } 24 | 25 | resource "aws_ssm_parameter" "github_client_id" { 26 | name = "AUTH_GITHUB_CLIENT_ID" 27 | type = "String" 28 | value = var.github_client_id 29 | } 30 | 31 | resource "aws_ssm_parameter" "github_client_secret" { 32 | name = "AUTH_GITHUB_CLIENT_SECRET" 33 | type = "String" 34 | value = var.github_client_secret 35 | } 36 | 37 | resource "aws_ssm_parameter" "access_key_id" { 38 | name = "ACCESS_KEY_ID" 39 | type = "String" 40 | value = var.access_key_id 41 | } 42 | 43 | resource "aws_ssm_parameter" "secret_access_key" { 44 | name = "SECRET_ACCESS_KEY" 45 | type = "String" 46 | value = var.secret_access_key 47 | } 48 | -------------------------------------------------------------------------------- /terraform/modules/ssm/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable postgres_host { 6 | type = string 7 | } 8 | 9 | variable postgres_user { 10 | type = string 11 | } 12 | 13 | variable postgres_password { 14 | type = string 15 | } 16 | 17 | variable github_token { 18 | type = string 19 | } 20 | 21 | variable github_client_id { 22 | type = string 23 | } 24 | 25 | variable github_client_secret { 26 | type = string 27 | } 28 | 29 | variable access_key_id { 30 | type = string 31 | } 32 | 33 | variable secret_access_key { 34 | type = string 35 | } 36 | -------------------------------------------------------------------------------- /terraform/modules/vpc/output.tf: -------------------------------------------------------------------------------- 1 | output vpc_id { 2 | value = aws_vpc.backstage_vpc.id 3 | } 4 | 5 | output security_group_id { 6 | value = aws_vpc.backstage_vpc.default_security_group_id 7 | } 8 | 9 | output public_subnets { 10 | value = { 11 | for subnet in aws_subnet.public_subnets: 12 | subnet.availability_zone => subnet.id 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /terraform/modules/vpc/variables.tf: -------------------------------------------------------------------------------- 1 | variable project { 2 | type = string 3 | } 4 | 5 | variable vpc_cidr_block { 6 | type = string 7 | } 8 | 9 | variable public_subnets { 10 | type = map 11 | } 12 | -------------------------------------------------------------------------------- /terraform/modules/vpc/vpc.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "backstage_vpc" { 2 | cidr_block = var.vpc_cidr_block 3 | enable_dns_support = true 4 | enable_dns_hostnames = true 5 | enable_classiclink = false 6 | instance_tenancy = "default" 7 | tags = { 8 | Name = "${var.project}-vpc" 9 | } 10 | } 11 | 12 | resource "aws_subnet" "public_subnets" { 13 | for_each = var.public_subnets 14 | vpc_id = aws_vpc.backstage_vpc.id 15 | cidr_block = each.value 16 | map_public_ip_on_launch = true 17 | availability_zone = each.key 18 | tags = { 19 | Name = "${var.project}-public-subnet-${each.key}" 20 | } 21 | } 22 | 23 | resource "aws_internet_gateway" "backstage_igw" { 24 | vpc_id = aws_vpc.backstage_vpc.id 25 | tags = { 26 | Name = "${var.project}-igw" 27 | } 28 | } 29 | 30 | resource "aws_route_table" "backstage_public_crt" { 31 | vpc_id = aws_vpc.backstage_vpc.id 32 | route { 33 | cidr_block = "0.0.0.0/0" 34 | gateway_id = aws_internet_gateway.backstage_igw.id 35 | } 36 | tags = { 37 | Name = "${var.project}-public-crt" 38 | } 39 | } 40 | 41 | resource "aws_route_table_association" "public_route"{ 42 | for_each = aws_subnet.public_subnets 43 | subnet_id = each.value.id 44 | route_table_id = aws_route_table.backstage_public_crt.id 45 | } 46 | -------------------------------------------------------------------------------- /terraform/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.0.2" 3 | 4 | backend "s3" { 5 | bucket = "{{BUCKET-NAME}}" 6 | key = "tf-state.json" 7 | region = "eu-west-1" 8 | workspace_key_prefix = "environment" 9 | } 10 | 11 | required_providers { 12 | aws = { 13 | source = "hashicorp/aws" 14 | version = "3.44.0" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # General 2 | project = "backstage" 3 | default_region = "eu-west-1" 4 | vpc_cidr_block = "172.31.0.0/16" 5 | public_subnets = { 6 | "eu-west-1a" = "172.31.0.0/20", 7 | "eu-west-1b" = "172.31.16.0/20" 8 | } 9 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # Variables 2 | variable default_region { 3 | type = string 4 | } 5 | 6 | variable project { 7 | type = string 8 | } 9 | 10 | variable vpc_cidr_block { 11 | type = string 12 | } 13 | 14 | variable public_subnets { 15 | type = map 16 | } 17 | 18 | variable docker_image_tag { 19 | type = string 20 | } 21 | 22 | variable postgres_user { 23 | type = string 24 | } 25 | 26 | variable postgres_password { 27 | type = string 28 | } 29 | 30 | variable github_token { 31 | type = string 32 | } 33 | 34 | variable github_client_id { 35 | type = string 36 | } 37 | 38 | variable github_client_secret { 39 | type = string 40 | } 41 | 42 | variable access_key_id { 43 | type = string 44 | } 45 | 46 | variable secret_access_key { 47 | type = string 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": [ 4 | "packages/*/src", 5 | "plugins/*/src", 6 | "plugins/*/dev", 7 | "plugins/*/migrations" 8 | ], 9 | "exclude": ["node_modules"], 10 | "compilerOptions": { 11 | "outDir": "dist-types", 12 | "rootDir": "." 13 | } 14 | } 15 | --------------------------------------------------------------------------------