├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------