PageRenderTime 60ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md

https://gitlab.com/artofhuman/gitlab-ce
Markdown | 526 lines | 435 code | 91 blank | 0 comment | 0 complexity | ee9aa415229e88ab232ba4d89c55c48f MD5 | raw file
  1. ---
  2. author: Ryan Hall
  3. author_gitlab: blitzgren
  4. level: intermediary
  5. article_type: tutorial
  6. date: 2018-03-07
  7. ---
  8. # DevOps and Game Dev with GitLab CI/CD
  9. With advances in WebGL and WebSockets, browsers are extremely viable as game development
  10. platforms without the use of plugins like Adobe Flash. Furthermore, by using GitLab and [AWS](https://aws.amazon.com/),
  11. single game developers, as well as game dev teams, can easily host browser-based games online.
  12. In this tutorial, we'll focus on DevOps, as well as testing and hosting games with Continuous
  13. Integration/Deployment methods. We assume you are familiar with GitLab, javascript,
  14. and the basics of game development.
  15. ## The game
  16. Our [demo game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/) consists of a simple spaceship traveling in space that shoots by clicking the mouse in a given direction.
  17. Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](http://darknova.io/about),
  18. was essential for the fast pace the team worked at. This tutorial will build upon my
  19. [previous introductory article](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) and go through the following steps:
  20. 1. Using code from the previous article to start with a barebones [Phaser](https://phaser.io) game built by a gulp file
  21. 1. Adding and running unit tests
  22. 1. Creating a `Weapon` class that can be triggered to spawn a `Bullet` in a given direction
  23. 1. Adding a `Player` class that uses this weapon and moves around the screen
  24. 1. Adding the sprites we will use for the `Player` and `Weapon`
  25. 1. Testing and deploying with Continuous Integration and Continuous Deployment methods
  26. By the end, we'll have the core of a [playable game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/)
  27. that's tested and deployed on every push to the `master` branch of the [codebase](https://gitlab.com/blitzgren/gitlab-game-demo).
  28. This will also provide
  29. boilerplate code for starting a browser-based game with the following components:
  30. - Written in [Typescript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io)
  31. - Building, running, and testing with [Gulp](http://gulpjs.com/)
  32. - Unit tests with [Chai](http://chaijs.com/) and [Mocha](https://mochajs.org/)
  33. - CI/CD with GitLab
  34. - Hosting the codebase on GitLab.com
  35. - Hosting the game on AWS
  36. - Deploying to AWS
  37. ## Requirements and setup
  38. Please refer to my previous article [DevOps and Game Dev](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) to learn the foundational
  39. development tools, running a Hello World-like game, and building this game using GitLab
  40. CI/CD from every new push to master. The `master` branch for this game's [repository](https://gitlab.com/blitzgren/gitlab-game-demo)
  41. contains a completed version with all configurations. If you would like to follow along
  42. with this article, you can clone and work from the `devops-article` branch:
  43. ```sh
  44. git clone git@gitlab.com:blitzgren/gitlab-game-demo.git
  45. git checkout devops-article
  46. ```
  47. Next, we'll create a small subset of tests that exemplify most of the states I expect
  48. this `Weapon` class to go through. To get started, create a folder called `lib/tests`
  49. and add the following code to a new file `weaponTests.ts`:
  50. ```ts
  51. import { expect } from 'chai';
  52. import { Weapon, BulletFactory } from '../lib/weapon';
  53. describe('Weapon', () => {
  54. var subject: Weapon;
  55. var shotsFired: number = 0;
  56. // Mocked bullet factory
  57. var bulletFactory: BulletFactory = <BulletFactory>{
  58. generate: function(px, py, vx, vy, rot) {
  59. shotsFired++;
  60. }
  61. };
  62. var parent: any = { x: 0, y: 0 };
  63. beforeEach(() => {
  64. shotsFired = 0;
  65. subject = new Weapon(bulletFactory, parent, 0.25, 1);
  66. });
  67. it('should shoot if not in cooldown', () => {
  68. subject.trigger(true);
  69. subject.update(0.1);
  70. expect(shotsFired).to.equal(1);
  71. });
  72. it('should not shoot during cooldown', () => {
  73. subject.trigger(true);
  74. subject.update(0.1);
  75. subject.update(0.1);
  76. expect(shotsFired).to.equal(1);
  77. });
  78. it('should shoot after cooldown ends', () => {
  79. subject.trigger(true);
  80. subject.update(0.1);
  81. subject.update(0.3); // longer than timeout
  82. expect(shotsFired).to.equal(2);
  83. });
  84. it('should not shoot if not triggered', () => {
  85. subject.update(0.1);
  86. subject.update(0.1);
  87. expect(shotsFired).to.equal(0);
  88. });
  89. });
  90. ```
  91. To build and run these tests using gulp, let's also add the following gulp functions
  92. to the existing `gulpfile.js` file:
  93. ```ts
  94. gulp.task('build-test', function () {
  95. return gulp.src('src/tests/**/*.ts', { read: false })
  96. .pipe(tap(function (file) {
  97. // replace file contents with browserify's bundle stream
  98. file.contents = browserify(file.path, { debug: true })
  99. .plugin(tsify, { project: "./tsconfig.test.json" })
  100. .bundle();
  101. }))
  102. .pipe(buffer())
  103. .pipe(sourcemaps.init({loadMaps: true}) )
  104. .pipe(gulp.dest('built/tests'));
  105. });
  106. gulp.task('run-test', function() {
  107. gulp.src(['./built/tests/**/*.ts']).pipe(mocha());
  108. });
  109. ```
  110. We will start implementing the first part of our game and get these `Weapon` tests to pass.
  111. The `Weapon` class will expose a method to trigger the generation of a bullet at a given
  112. direction and speed. Later we will implement a `Player` class that ties together the user input
  113. to trigger the weapon. In the `src/lib` folder create a `weapon.ts` file. We'll add two classes
  114. to it: `Weapon` and `BulletFactory` which will encapsulate Phaser's **sprite** and
  115. **group** objects, and the logic specific to our game.
  116. ```ts
  117. export class Weapon {
  118. private isTriggered: boolean = false;
  119. private currentTimer: number = 0;
  120. constructor(private bulletFactory: BulletFactory, private parent: Phaser.Sprite, private cooldown: number, private bulletSpeed: number) {
  121. }
  122. public trigger(on: boolean): void {
  123. this.isTriggered = on;
  124. }
  125. public update(delta: number): void {
  126. this.currentTimer -= delta;
  127. if (this.isTriggered && this.currentTimer <= 0) {
  128. this.shoot();
  129. }
  130. }
  131. private shoot(): void {
  132. // Reset timer
  133. this.currentTimer = this.cooldown;
  134. // Get velocity direction from player rotation
  135. var parentRotation = this.parent.rotation + Math.PI / 2;
  136. var velx = Math.cos(parentRotation);
  137. var vely = Math.sin(parentRotation);
  138. // Apply a small forward offset so bullet shoots from head of ship instead of the middle
  139. var posx = this.parent.x - velx * 10
  140. var posy = this.parent.y - vely * 10;
  141. this.bulletFactory.generate(posx, posy, -velx * this.bulletSpeed, -vely * this.bulletSpeed, this.parent.rotation);
  142. }
  143. }
  144. export class BulletFactory {
  145. constructor(private bullets: Phaser.Group, private poolSize: number) {
  146. // Set all the defaults for this BulletFactory's bullet object
  147. this.bullets.enableBody = true;
  148. this.bullets.physicsBodyType = Phaser.Physics.ARCADE;
  149. this.bullets.createMultiple(30, 'bullet');
  150. this.bullets.setAll('anchor.x', 0.5);
  151. this.bullets.setAll('anchor.y', 0.5);
  152. this.bullets.setAll('outOfBoundsKill', true);
  153. this.bullets.setAll('checkWorldBounds', true);
  154. }
  155. public generate(posx: number, posy: number, velx: number, vely: number, rot: number): Phaser.Sprite {
  156. // Pull a bullet from Phaser's Group pool
  157. var bullet = this.bullets.getFirstExists(false);
  158. // Set the few unique properties about this bullet: rotation, position, and velocity
  159. if (bullet) {
  160. bullet.reset(posx, posy);
  161. bullet.rotation = rot;
  162. bullet.body.velocity.x = velx;
  163. bullet.body.velocity.y = vely;
  164. }
  165. return bullet;
  166. }
  167. }
  168. ```
  169. Lastly, we'll redo our entry point, `game.ts`, to tie together both `Player` and `Weapon` objects
  170. as well as add them to the update loop. Here is what the updated `game.ts` file looks like:
  171. ```ts
  172. import { Player } from "./player";
  173. import { Weapon, BulletFactory } from "./weapon";
  174. window.onload = function() {
  175. var game = new Phaser.Game(800, 600, Phaser.AUTO, 'gameCanvas', { preload: preload, create: create, update: update });
  176. var player: Player;
  177. var weapon: Weapon;
  178. // Import all assets prior to loading the game
  179. function preload () {
  180. game.load.image('player', 'assets/player.png');
  181. game.load.image('bullet', 'assets/bullet.png');
  182. }
  183. // Create all entities in the game, after Phaser loads
  184. function create () {
  185. // Create and position the player
  186. var playerSprite = game.add.sprite(400, 550, 'player');
  187. playerSprite.anchor.setTo(0.5);
  188. player = new Player(game.input, playerSprite, 150);
  189. var bulletFactory = new BulletFactory(game.add.group(), 30);
  190. weapon = new Weapon(bulletFactory, player.sprite, 0.25, 1000);
  191. player.loadWeapon(weapon);
  192. }
  193. // This function is called once every tick, default is 60fps
  194. function update() {
  195. var deltaSeconds = game.time.elapsedMS / 1000; // convert to seconds
  196. player.update(deltaSeconds);
  197. weapon.update(deltaSeconds);
  198. }
  199. }
  200. ```
  201. Run `gulp serve` and you can run around and shoot. Wonderful! Let's update our CI
  202. pipeline to include running the tests along with the existing build job.
  203. ## Continuous Integration
  204. To ensure our changes don't break the build and all tests still pass, we utilize
  205. Continuous Integration (CI) to run these checks automatically for every push.
  206. Read through this article to understand [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/),
  207. and how these methods are leveraged by GitLab.
  208. From the [last tutorial](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) we already have a `.gitlab-ci.yml` file set up for building our app from
  209. every push. We need to set up a new CI job for testing, which GitLab CI/CD will run after the build job using our generated artifacts from gulp.
  210. Please read through the [documentation on CI/CD configuration](../../../ci/yaml/README.md) file to explore its contents and adjust it to your needs.
  211. ### Build your game with GitLab CI/CD
  212. We need to update our build job to ensure tests get run as well. Add `gulp build-test`
  213. to the end of the `script` array for the existing `build` job. Once these commands run,
  214. we know we will need to access everything in the `built` folder, given by GitLab CI/CD's `artifacts`.
  215. We'll also cache `node_modules` to avoid having to do a full re-pull of those dependencies:
  216. just pack them up in the cache. Here is the full `build` job:
  217. ```yml
  218. build:
  219. stage: build
  220. script:
  221. - npm i gulp -g
  222. - npm i
  223. - gulp
  224. - gulp build-test
  225. cache:
  226. policy: push
  227. paths:
  228. - node_modules
  229. artifacts:
  230. paths:
  231. - built
  232. ```
  233. ### Test your game with GitLab CI/CD
  234. For testing locally, we simply run `gulp run-tests`, which requires gulp to be installed
  235. globally like in the `build` job. We pull `node_modules` from the cache, so the `npm i`
  236. command won't have to do much. In preparation for deployment, we know we will still need
  237. the `built` folder in the artifacts, which will be brought over as default behavior from
  238. the previous job. Lastly, by convention, we let GitLab CI/CD know this needs to be run after
  239. the `build` job by giving it a `test` [stage](../../../ci/yaml/README.md#stages).
  240. Following the YAML structure, the `test` job should look like this:
  241. ```yml
  242. test:
  243. stage: test
  244. script:
  245. - npm i gulp -g
  246. - npm i
  247. - gulp run-test
  248. cache:
  249. policy: push
  250. paths:
  251. - node_modules/
  252. artifacts:
  253. paths:
  254. - built/
  255. ```
  256. We have added unit tests for a `Weapon` class that shoots on a specified interval.
  257. The `Player` class implements `Weapon` along with the ability to move around and shoot. Also,
  258. we've added test artifacts and a test stage to our GitLab CI/CD pipeline using `.gitlab-ci.yml`,
  259. allowing us to run our tests by every push.
  260. Our entire `.gitlab-ci.yml` file should now look like this:
  261. ```yml
  262. image: node:6
  263. build:
  264. stage: build
  265. script:
  266. - npm i gulp -g
  267. - npm i
  268. - gulp
  269. - gulp build-test
  270. cache:
  271. policy: push
  272. paths:
  273. - node_modules/
  274. artifacts:
  275. paths:
  276. - built/
  277. test:
  278. stage: test
  279. script:
  280. - npm i gulp -g
  281. - npm i
  282. - gulp run-test
  283. cache:
  284. policy: pull
  285. paths:
  286. - node_modules/
  287. artifacts:
  288. paths:
  289. - built/
  290. ```
  291. ### Run your CI/CD pipeline
  292. That's it! Add all your new files, commit, and push. For a reference of what our repo should
  293. look like at this point, please refer to the [final commit related to this article on my sample repository](https://gitlab.com/blitzgren/gitlab-game-demo/commit/8b36ef0ecebcf569aeb251be4ee13743337fcfe2).
  294. By applying both build and test stages, GitLab will run them sequentially at every push to
  295. our repository. If all goes well you'll end up with a green check mark on each job for the pipeline:
  296. ![Passing Pipeline](img/test_pipeline_pass.png)
  297. You can confirm that the tests passed by clicking on the `test` job to enter the full build logs.
  298. Scroll to the bottom and observe, in all its passing glory:
  299. ```sh
  300. $ gulp run-test
  301. [18:37:24] Using gulpfile /builds/blitzgren/gitlab-game-demo/gulpfile.js
  302. [18:37:24] Starting 'run-test'...
  303. [18:37:24] Finished 'run-test' after 21 ms
  304. Weapon
  305. ✓ should shoot if not in cooldown
  306. ✓ should not shoot during cooldown
  307. ✓ should shoot after cooldown ends
  308. ✓ should not shoot if not triggered
  309. 4 passing (18ms)
  310. Uploading artifacts...
  311. built/: found 17 matching files
  312. Uploading artifacts to coordinator... ok id=17095874 responseStatus=201 Created token=aaaaaaaa Job succeeded
  313. ```
  314. ## Continuous Deployment
  315. We have our codebase built and tested on every push. To complete the full pipeline with Continuous Deployment,
  316. let's set up [free web hosting with AWS S3](https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/) and a job through which our build artifacts get
  317. deployed. GitLab also has a free static site hosting service we could use, [GitLab Pages](https://about.gitlab.com/features/pages/),
  318. however Dark Nova specifically uses other AWS tools that necessitates using `AWS S3`.
  319. Read through this article that describes [deploying to both S3 and GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
  320. and further delves into the principles of GitLab CI/CD than discussed in this article.
  321. ### Set up S3 Bucket
  322. 1. Log into your AWS account and go to [S3](https://console.aws.amazon.com/s3/home)
  323. 1. Click the **Create Bucket** link at the top
  324. 1. Enter a name of your choosing and click next
  325. 1. Keep the default **Properties** and click next
  326. 1. Click the **Manage group permissions** and allow **Read** for the **Everyone** group, click next
  327. 1. Create the bucket, and select it in your S3 bucket list
  328. 1. On the right side, click **Properties** and enable the **Static website hosting** category
  329. 1. Update the radio button to the **Use this bucket to host a website** selection. Fill in `index.html` and `error.html` respectively
  330. ### Set up AWS Secrets
  331. We need to be able to deploy to AWS with our AWS account credentials, but we certainly
  332. don't want to put secrets into source code. Luckily GitLab provides a solution for this
  333. with [Variables](../../../ci/variables/README.md). This can get complicated
  334. due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't
  335. use root security credentials. Proper IAM credential management is beyond the scope of this
  336. article, but AWS will remind you that using root credentials is unadvised and against their
  337. best practices, as they should. Feel free to follow best practices and use a custom IAM user's
  338. credentials, which will be the same two credentials (Key ID and Secret). It's a good idea to
  339. fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab:
  340. 1. Log into your AWS account and go to the [Security Credentials page](https://console.aws.amazon.com/iam/home#/security_credential)
  341. 1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the id and secret around, you'll need them later
  342. ![AWS Access Key Config](img/aws_config_window.png)
  343. 1. Go to your GitLab project, click **Settings > CI/CD** on the left sidebar
  344. 1. Expand the **Variables** section
  345. ![GitLab Secret Config](img/gitlab_config.png)
  346. 1. Add a key named `AWS_KEY_ID` and copy the key id from Step 2 into the **Value** textbox
  347. 1. Add a key named `AWS_KEY_SECRET` and copy the key secret from Step 2 into the **Value** textbox
  348. ### Deploy your game with GitLab CI/CD
  349. To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on
  350. the Shared Runner. The Shared Runner also needs to be able to authenticate with your AWS
  351. account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID`
  352. and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the variables we
  353. set up in the prior section using the `variables` portion of the `deploy` job. At the end,
  354. we add directives to ensure deployment `only` happens on pushes to `master`. This way, every
  355. single branch still runs through CI, and only merging (or committing directly) to master will
  356. trigger the `deploy` job of our pipeline. Put these together to get the following:
  357. ```yml
  358. deploy:
  359. stage: deploy
  360. variables:
  361. AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
  362. AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
  363. script:
  364. - apt-get update
  365. - apt-get install -y python3-dev python3-pip
  366. - easy_install3 -U pip
  367. - pip3 install --upgrade awscli
  368. - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete
  369. only:
  370. - master
  371. ```
  372. Be sure to update the region and S3 URL in that last script command to fit your setup.
  373. Our final configuration file `.gitlab-ci.yml` looks like:
  374. ```yml
  375. image: node:6
  376. build:
  377. stage: build
  378. script:
  379. - npm i gulp -g
  380. - npm i
  381. - gulp
  382. - gulp build-test
  383. cache:
  384. policy: push
  385. paths:
  386. - node_modules/
  387. artifacts:
  388. paths:
  389. - built/
  390. test:
  391. stage: test
  392. script:
  393. - npm i gulp -g
  394. - gulp run-test
  395. cache:
  396. policy: pull
  397. paths:
  398. - node_modules/
  399. artifacts:
  400. paths:
  401. - built/
  402. deploy:
  403. stage: deploy
  404. variables:
  405. AWS_ACCESS_KEY_ID: "$AWS_KEY_ID"
  406. AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET"
  407. script:
  408. - apt-get update
  409. - apt-get install -y python3-dev python3-pip
  410. - easy_install3 -U pip
  411. - pip3 install --upgrade awscli
  412. - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete
  413. only:
  414. - master
  415. ```
  416. ## Conclusion
  417. Within the [demo repository](https://gitlab.com/blitzgren/gitlab-game-demo) you can also find a handful of boilerplate code to get
  418. [Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](http://gulpjs.com/) and [Phaser](https://phaser.io) all playing
  419. together nicely with GitLab CI/CD, which is the result of lessons learned while making [Dark Nova](http://darknova.io/).
  420. Using a combination of free and open source software, we have a full CI/CD pipeline, a game foundation,
  421. and unit tests, all running and deployed at every push to master - with shockingly little code.
  422. Errors can be easily debugged through GitLab's build logs, and within minutes of a successful commit,
  423. you can see the changes live on your game.
  424. Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables
  425. rapid but stable development. We can easily test changes in a separate [environment](../../../ci/environments.md#introduction-to-environments-and-deployments),
  426. or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing
  427. and tedious, but having faith in a stable deployment with GitLab CI/CD allows
  428. a lot of breathing room in quickly getting changes to players.
  429. ## Further settings
  430. Here are some ideas to further investigate that can speed up or improve your pipeline:
  431. - [Yarn](https://yarnpkg.com) instead of npm
  432. - Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ci-yml) image that can preload dependencies and tools (like AWS CLI)
  433. - Forward a [custom domain](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website
  434. - Combine jobs if you find it unnecessary for a small project
  435. - Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)