[{"data":1,"prerenderedAt":759},["ShallowReactive",2],{"/en-us/blog/path-to-decomposing-gitlab-database-part1":3,"navigation-en-us":35,"banner-en-us":463,"footer-en-us":480,"Dylan Griffith":725,"next-steps-en-us":738,"footer-source-/en-us/blog/path-to-decomposing-gitlab-database-part1/":753},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":25,"_id":28,"_type":29,"title":30,"_source":31,"_file":32,"_stem":33,"_extension":34},"/en-us/blog/path-to-decomposing-gitlab-database-part1","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Decomposing the GitLab backend database, Part 1: Designing and planning","A technical summary of the yearlong project to decompose GitLab's Postgres database. This first part focuses on the initial designing and planning of the project.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749663397/Blog/Hero%20Images/logoforblogpost.jpg","https://about.gitlab.com/blog/path-to-decomposing-gitlab-database-part1","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"Decomposing the GitLab backend database, Part 1: Designing and planning\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Dylan Griffith\"}],\n        \"datePublished\": \"2022-08-04\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21,"tags":22},[18],"Dylan Griffith","2022-08-04","\nRecently we finished [migrating the GitLab.com monolithic Postgres database to two independent databases: `Main` and `CI`](/blog/splitting-database-into-main-and-ci/). After we decided how to split things up, the project took about a year to complete.\n\nThis blog post on decomposing the GitLab backend database is part one in a three-part series. The posts give technical details about many of the challenges we had to\novercome, as well as links to issues, merge requests, epics, and developer-facing documentation.\nOur hope is that you can get as much detail as you want about how we work on complex projects at GitLab.\n\nWe highlight the most interesting details, but anyone undertaking a similar\nproject might learn a lot from seeing all\nthe different trade-offs we evaluated along the way.\n\n- \"Decomposing the GitLab backend database, Part 1\" focuses on the initial design and planning of the project.\n- [Part 2](/blog/path-to-decomposing-gitlab-database-part2/) focuses on the\nexecution of the final migration.\n- [Part 3](/blog/path-to-decomposing-gitlab-database-part3/) highlights some interesting technical challenges we had to solve along the way, as well as some surprises.\n\n## How it began\n\nBack in early 2021, GitLab formed a \"database sharding\" team in an effort to\ndeal with our ever-growing monolithic Postgres database. This database stored\nalmost all the data generated by GitLab.com users, excluding git data and some other\nsmaller things.\n\nAs this database grew over time, it became a common source of\nincidents for GitLab. We knew that eventually we had to move away from a single\nPostgres database. We were already approaching the limits of what we could do\non a single VM with 96 vCPU and continually trying to vertically scale this VM\nwould eventually not be possible. Even if we could vertically scale forever,\nmanaging such a large Postgres database just becomes more and more difficult.\n\nEven though our database architecture has been monolithic for a long time, we already made use of many scaling techniques, including:\n\n- Using Patroni to have a pool of replicas for read-only traffic\n- Using PGBouncer for pooling the vast number of connections across our application fleet\n\n![Database architecture before decomposition](https://about.gitlab.com/images/blogimages/2022-07-15-path-to-decomposing-gitlab-database/phase0.png)\n\nThese approaches only got us so far and ultimately would never fix the scaling\nbottleneck of the number of writes that need to happen, because all writes need to\ngo to the primary database.\n\nThe original objective of the database sharding team was to find a viable way\nto horizontally shard the data in the database. We started with exploring\n[sharding by top-level namespace][sharding_by_top_level_namespace_poc_epic]. This approach had some very complicated problems to solve, because the application\nwas never designed to have strict tenancy boundaries around top-level\nnamespaces. We believe that ultimately this will be a good way to split and\nscale the database, but we needed a shorter term solution to our scaling\nproblems.\n\nThis is when we evaluated different ways to extract certain tables into a\nseparate database. This approach is often referred to as \"vertical\npartitioning\" or \"functional decomposition.\" We assumed this extraction would likely\nbe easier, as long as we found a set of tables with loose coupling to the rest\nof the database. We knew it would require us to remove all joins to the rest of the\ntables (more on that later).\n\n## Figuring out where most write activity occurs\n\nWe did [an analysis][analysis_of_decomposition_tables] of:\n\n- Where the bulk of our data was stored\n- The write traffic (since ultimately the number of writes was the thing we were trying to reduce)\n\nWe learned that CI tables (at the time) made up around 40% to 50% of our write traffic. This seemed like a\nperfect candidate, because splitting the database in half (by write traffic) would be\nthe optimal scaling step.\n\nWe analyzed the data by splitting the database the following ways:\n\n| Tables group   | DB size (GB) | DB size (%) | Reads/s   | Reads/s (%) | Writes/s | Writes/s (%) |\n|----------------|--------------|-------------|-----------|-------------|----------|--------------|\n| Webhook logs   | 2964.1       | 22.39%      | 52.5      | 0.00%       | 110.0    | 2.82%        |\n| Merge Requests | 2673.7       | 20.20%      | 126073.4  | 1.31%       | 795.4    | 20.40%       |\n| CI             | 4725.0       | 35.69%      | 1712843.8 | 17.87%      | 1909.2   | 48.98%       |\n| Rest           | 2876.3       | 21.73%      | 7748488.5 | 80.82%      | 1083.6   | 27.80%       |\n\nChoosing to split the CI tables from the database was partly based on instinct.\nWe knew the CI tables (particularly `ci_builds` and\nrelated metadata) were already some of the largest tables in our database. It\nwas also a convenient choice because the CI tables were already prefixed with\n`ci_`. In the end, we realized only three tables were CI tables that weren't\nprefixed with `ci_`. You can see the up-to-date list of tables and their respective\ndatabase in [`gitlab_schemas.yml`][gitlab_schemas_yml].\n\nThe next step was to see how viable it actually was.\n\n## Proving it can work\n\nThe [first proof-of-concept merge request][initial_poc_mr_for_ci_decomposition] was created\nin August 2021. The proof-of-concept process involved:\n\n- Separating the database and seeing what broke\n- Fixing blockers and marking todo's until we ended up with the application \"pretty much working\"\n\nWe never merged this proof of concept, but we progressively broke out changes into smaller merge requests\nor issues assigned to the appropriate teams to fix.\n\n![Screenshot of large proof-of-concept MR](https://about.gitlab.com/images/blogimages/2022-07-15-path-to-decomposing-gitlab-database/poc-mr-scale.png)\n\n## Chasing a moving target\n\nWhen tackling a large-scale architecture change, you might find\nyourself chasing a moving target.\n\nTo split the database, we had to change the application. Our code depended on all\nthe tables being in a single database. These changes took almost a year.\n\nIn the meantime, the application was constantly evolving\nand growing, and with contributions from many engineers who weren't necessarily\nfamiliar with the CI decomposition project. This meant that we couldn't just\nstart fixing problems. We knew we would likely find new problems being\nintroduced at a faster rate than we could remove them.\n\nTo solve this problem, we took an approach that was inspired by\n[how we handle new RuboCop rules](https://docs.gitlab.com/ee/development/contributing/style_guides.html#resolving-rubocop-exceptions).\nThe idea is to implement static or dynamic analysis to detect these\nproblems. Then we use this information to generate an allowlist of exceptions.\nAfter we have this allowlist of exceptions, we prevent any new violations from being created\n(as any new violations will fail the pipeline).\n\nThe result was a clear list to work on and visibility into our progress.\n\nAs part of making the application compatible with CI decomposition, we needed to\nbuild the following:\n\n- [Multiple databases documentation][docs_multiple_databases] taught\n  developers how to write code that is compatible with multiple databases.\n- [Cross-join detection][mr_cross_join_detection] analyzed all SQL queries\n  and raised an error if the query spanned multiple databases.\n- [Cross-database transaction detection][mr_cross_db_transaction_detection]\n  analyzed all transactions and raised an error if queries were sent to two\n  different databases within the context of a single transaction.\n- [Query analyzer metrics][mr_query_analyzer_metrics] analyzed all SQL queries\n  and tracked the different databases that would be queried (based on table\n  names). These metrics, which were sampled at a rate of 1/10,000 queries, because they are\n  expensive to parse, were sent to Prometheus. We used this data to get a sense\n  of whether we were whittling down the list of cross-joins in production.\n  It also helped us catch code paths that weren't covered by tests but were\n  executed in production.\n- [A Rubocop rule for preventing the use of\n  `ActiveRecord::Base`][mr_rubocop_rule_ar_base] ensured that we always\n  used an explicit database connection for Main or CI.\n\n## Using Rails multiple database support\n\nWhen we began this project, there were many improvements being added to Rails to\nsupport multiple databases. We wanted to make use of as much of this Rails\nbuilt-in support as possible to minimize the amount of custom database\nconnection logic we had to maintain.\n\nOne considerable challenge with this was our existing\n[custom database load balancing logic](https://docs.gitlab.com/ee/administration/postgresql/database_load_balancing.html).\nThe development of this complex implementation spans a long period of time, and\nit was designed differently to how Rails connections were managed in the new\nmulti-database support.\n\nIn the end, were able to use parts of Rails multiple database support, but\n[we still hope to one day remove our custom logic and only use what is supported by Rails][epic_to_move_to_native_rails_multiple_dataabase_support].\n\n## Implementing loose foreign keys\n\nThere were still some foreign keys that existed between CI and non-CI tables.\nWe needed a way to remove these keys but still keep the functionality of cascading\ndeletes.\n\nIn the end, [we implemented a solution][lfk_mr]\nwe call [\"loose foreign keys\"][lfk_docs]. This solution provides similar functionality and\nsupport for cascading `NULLIFY` or `DELETE` when a parent record is deleted in\nPostgres. It's implemented using Postgres on delete triggers, so it guarantees all\ndeletes (including bulk deletes) will be handled. The trigger writes to another\n\"queue\" table in Postgres, which then is picked up by a periodic Sidekiq worker\nto clean up all the impacted child records.\n\nWhen implementing this solution, we also considered the option of using\n[`ActiveRecord` `before_destroy` callbacks](https://apidock.com/rails/ActiveRecord/Callbacks/before_destroy).\nHowever they couldn't give us the same guarantees as Postgres foreign keys,\nbecause they can be intentionally or accidentally skipped.\n\nIn the end, the \"loose foreign keys\" solution also helped to solve another problem\nwe have, where very large cascading deletes cause timeouts and user experience issues.\nBecause it's asynchronous, we could easily control timing and batch sizes to never\nhave database timeouts and never overload the database with a single large\ndelete.\n\n## Mirroring namespaces and projects\n\nOne of the most difficult dependencies between CI and Main features in GitLab\nis how CI Runners are configured. Runners are assigned to projects and groups\nwhich then dictates which jobs they will run. This meant there were many join\nqueries from the `ci_runners` table to the `projects` and `namespaces` tables.\nWe solved most of these issues by refactoring our Rails code and queries, but\nsome proved very difficult to do efficiently.\n\nTo work around this issue, [we implemented][mr_namespace_project_mirroring] a mechanism to\n[mirror the relevant columns on `projects` and `namespaces` to the CI database][docs_ci_mirrored_tables].\n\nIt's not ideal to have to duplicate data that must be kept up-to-date like\nthis, but while we expected this may be necessary in a few places, it turns out\nthat we only ended up doing this for those two tables. All other joins could be\nhandled without mirroring.\n\nAn important part of our mirroring architecture is periodic\n[consistency checking][mr_namespace_project_mirroring_consistency_check].\nEvery time this process runs, it takes a batch of the mirrored rows and compares them\nwith the expected values. If there is a discrepancy, it schedules them to be fixed.\nAfter it's done with this batch, it updates a cursor in Redis to be used for the\nnext batch.\n\n## Creating a phased rollout strategy\n\nA key part of ensuring our live migration went as smooth as possible was by\nmaking it as small as possible. This was quite difficult as the migration from\n1 database to 2 databases is a discrete change that seems hard to break up into\nsmaller steps that can be rolled out individually.\n\nOne [early insight][initial_migration_plan_mr] was that we could actually reconfigure GitLab.com ahead of\ntime so that the Rails application behaved as though it was talking to two\nseparate databases long before we actually split the databases. Basically the\nidea was that the Rails processes already had two separate database connections,\nbut ultimately they were going to the same database. We could even break things\nout further since our read-only connections are designed to read from slightly\ndelayed replicas. So we could already have read-only connections going to the\nnewly created CI read-only replicas before the migration.\n\n![Database architecture before final migration step](https://about.gitlab.com/images/blogimages/2022-07-15-path-to-decomposing-gitlab-database/phase4.png)\n\nThese insights led to our [seven-phase migration process][phased_migration_epic].\nThis process meant that by the time we got to the final migration on production\n(Phase 7), we were already incredibly confident that the application would work\nwith separate databases and the actual change being shipped was just trivial\nreconfiguration of a single database host. This also meant that all phases\n(except for Phase 7) had a very trivial rollback process, introduced very\nlittle risk of incident and could be shipped before we were finished with every\ncode change necessary to make the application support two databases.\n\nThe seven phases were:\n\n1. Deploy a Patroni cluster\n2. Configure Patroni standby cluster\n3. Serve CI reads from CI standby cluster\n4. Separate write connections for CI and Main (still going to the same primary host)\n5. Do a staging dry run and finishing the migration plan\n6. Validate metrics and additional logging\n7. Promote the CI database and send writes to it\n\n## Using labels to distribute work and prioritize\n\nNow that we had a clear set of phases we could prioritize our work. All issues\nwere assigned [scoped labels](https://docs.gitlab.com/ee/user/project/labels.html#scoped-labels)\nbased on the specific phase they corresponded to. Since the work spanned many\nteams in development and infrastructure, those teams could use the\nlabel to easily tell which issues needed to be worked on first. Additionally,\nsince we kept an up-to-date timeline of when we expected to ship each phase,\neach team could use the phase label to determine a rough deadline of when that\nwork should get done to not delay the project. Overall there were at least 193\nissues over all phases. Phase 1 and 2 were mostly infrastructure tasks tracked\nin a different group and with different labels, but the other phases contained\nthe bulk of the development team requirements:\n\n1. [8 Phase 3 issues](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=ci-decomposition%3A%3Aphase3)\n1. [78 Phase 4 issues](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=ci-decomposition%3A%3Aphase4)\n1. [7 Phase 5 issues](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=ci-decomposition%3A%3Aphase5)\n1. [64 Phase 6 issues](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=ci-decomposition%3A%3Aphase6)\n1. [34 Phase 7 issues](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=ci-decomposition%3A%3Aphase7)\n\n## Continue reading\n\nYou can read more about the final migration process and results of the migration in [Part 2](/blog/path-to-decomposing-gitlab-database-part2/).\n\n[initial_poc_mr_for_ci_decomposition]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67486\n[initial_migration_plan_mr]: https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/84588\n[lfk_mr]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69165\n[lfk_docs]: https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html\n[epic_to_move_to_native_rails_multiple_dataabase_support]: https://gitlab.com/gitlab-org/gitlab/-/issues/296870\n[phased_migration_epic]: https://gitlab.com/groups/gitlab-org/-/epics/6160\n[sharding_by_top_level_namespace_poc_epic]: https://gitlab.com/groups/gitlab-org/-/epics/5838\n[analysis_of_decomposition_tables]: https://gitlab.com/groups/gitlab-org/-/epics/5883#summary-of-impact\n[gitlab_schemas_yml]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/gitlab_schemas.yml\n[docs_ci_mirrored_tables]: https://docs.gitlab.com/ee/development/database/ci_mirrored_tables.html\n[mr_cross_join_detection]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68620\n[mr_cross_db_transaction_detection]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67213\n[mr_query_analyzer_metrics]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73839\n[mr_rubocop_rule_ar_base]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64937\n[mr_namespace_project_mirroring]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517\n[mr_namespace_project_mirroring_consistency_check]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81836\n[docs_multiple_databases]: https://docs.gitlab.com/ee/development/database/multiple_databases.html\n","engineering",[23,24],"inside GitLab","design",{"slug":26,"featured":6,"template":27},"path-to-decomposing-gitlab-database-part1","BlogPost","content:en-us:blog:path-to-decomposing-gitlab-database-part1.yml","yaml","Path To Decomposing Gitlab Database Part1","content","en-us/blog/path-to-decomposing-gitlab-database-part1.yml","en-us/blog/path-to-decomposing-gitlab-database-part1","yml",{"_path":36,"_dir":37,"_draft":6,"_partial":6,"_locale":7,"data":38,"_id":459,"_type":29,"title":460,"_source":31,"_file":461,"_stem":462,"_extension":34},"/shared/en-us/main-navigation","en-us",{"logo":39,"freeTrial":44,"sales":49,"login":54,"items":59,"search":390,"minimal":421,"duo":440,"pricingDeployment":449},{"config":40},{"href":41,"dataGaName":42,"dataGaLocation":43},"/","gitlab logo","header",{"text":45,"config":46},"Get free trial",{"href":47,"dataGaName":48,"dataGaLocation":43},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":50,"config":51},"Talk to sales",{"href":52,"dataGaName":53,"dataGaLocation":43},"/sales/","sales",{"text":55,"config":56},"Sign in",{"href":57,"dataGaName":58,"dataGaLocation":43},"https://gitlab.com/users/sign_in/","sign in",[60,104,201,206,311,371],{"text":61,"config":62,"cards":64,"footer":87},"Platform",{"dataNavLevelOne":63},"platform",[65,71,79],{"title":61,"description":66,"link":67},"The most comprehensive AI-powered DevSecOps Platform",{"text":68,"config":69},"Explore our Platform",{"href":70,"dataGaName":63,"dataGaLocation":43},"/platform/",{"title":72,"description":73,"link":74},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":75,"config":76},"Meet GitLab Duo",{"href":77,"dataGaName":78,"dataGaLocation":43},"/gitlab-duo/","gitlab duo ai",{"title":80,"description":81,"link":82},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":83,"config":84},"Learn more",{"href":85,"dataGaName":86,"dataGaLocation":43},"/why-gitlab/","why gitlab",{"title":88,"items":89},"Get started with",[90,95,100],{"text":91,"config":92},"Platform Engineering",{"href":93,"dataGaName":94,"dataGaLocation":43},"/solutions/platform-engineering/","platform engineering",{"text":96,"config":97},"Developer Experience",{"href":98,"dataGaName":99,"dataGaLocation":43},"/developer-experience/","Developer experience",{"text":101,"config":102},"MLOps",{"href":103,"dataGaName":101,"dataGaLocation":43},"/topics/devops/the-role-of-ai-in-devops/",{"text":105,"left":106,"config":107,"link":109,"lists":113,"footer":183},"Product",true,{"dataNavLevelOne":108},"solutions",{"text":110,"config":111},"View all Solutions",{"href":112,"dataGaName":108,"dataGaLocation":43},"/solutions/",[114,139,162],{"title":115,"description":116,"link":117,"items":122},"Automation","CI/CD and automation to accelerate deployment",{"config":118},{"icon":119,"href":120,"dataGaName":121,"dataGaLocation":43},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[123,127,131,135],{"text":124,"config":125},"CI/CD",{"href":126,"dataGaLocation":43,"dataGaName":124},"/solutions/continuous-integration/",{"text":128,"config":129},"AI-Assisted Development",{"href":77,"dataGaLocation":43,"dataGaName":130},"AI assisted development",{"text":132,"config":133},"Source Code Management",{"href":134,"dataGaLocation":43,"dataGaName":132},"/solutions/source-code-management/",{"text":136,"config":137},"Automated Software Delivery",{"href":120,"dataGaLocation":43,"dataGaName":138},"Automated software delivery",{"title":140,"description":141,"link":142,"items":147},"Security","Deliver code faster without compromising security",{"config":143},{"href":144,"dataGaName":145,"dataGaLocation":43,"icon":146},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[148,152,157],{"text":149,"config":150},"Application Security Testing",{"href":144,"dataGaName":151,"dataGaLocation":43},"Application security testing",{"text":153,"config":154},"Software Supply Chain Security",{"href":155,"dataGaLocation":43,"dataGaName":156},"/solutions/supply-chain/","Software supply chain security",{"text":158,"config":159},"Software Compliance",{"href":160,"dataGaName":161,"dataGaLocation":43},"/solutions/software-compliance/","software compliance",{"title":163,"link":164,"items":169},"Measurement",{"config":165},{"icon":166,"href":167,"dataGaName":168,"dataGaLocation":43},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[170,174,178],{"text":171,"config":172},"Visibility & Measurement",{"href":167,"dataGaLocation":43,"dataGaName":173},"Visibility and Measurement",{"text":175,"config":176},"Value Stream Management",{"href":177,"dataGaLocation":43,"dataGaName":175},"/solutions/value-stream-management/",{"text":179,"config":180},"Analytics & Insights",{"href":181,"dataGaLocation":43,"dataGaName":182},"/solutions/analytics-and-insights/","Analytics and insights",{"title":184,"items":185},"GitLab for",[186,191,196],{"text":187,"config":188},"Enterprise",{"href":189,"dataGaLocation":43,"dataGaName":190},"/enterprise/","enterprise",{"text":192,"config":193},"Small Business",{"href":194,"dataGaLocation":43,"dataGaName":195},"/small-business/","small business",{"text":197,"config":198},"Public Sector",{"href":199,"dataGaLocation":43,"dataGaName":200},"/solutions/public-sector/","public sector",{"text":202,"config":203},"Pricing",{"href":204,"dataGaName":205,"dataGaLocation":43,"dataNavLevelOne":205},"/pricing/","pricing",{"text":207,"config":208,"link":210,"lists":214,"feature":298},"Resources",{"dataNavLevelOne":209},"resources",{"text":211,"config":212},"View all resources",{"href":213,"dataGaName":209,"dataGaLocation":43},"/resources/",[215,248,270],{"title":216,"items":217},"Getting started",[218,223,228,233,238,243],{"text":219,"config":220},"Install",{"href":221,"dataGaName":222,"dataGaLocation":43},"/install/","install",{"text":224,"config":225},"Quick start guides",{"href":226,"dataGaName":227,"dataGaLocation":43},"/get-started/","quick setup checklists",{"text":229,"config":230},"Learn",{"href":231,"dataGaLocation":43,"dataGaName":232},"https://university.gitlab.com/","learn",{"text":234,"config":235},"Product documentation",{"href":236,"dataGaName":237,"dataGaLocation":43},"https://docs.gitlab.com/","product documentation",{"text":239,"config":240},"Best practice videos",{"href":241,"dataGaName":242,"dataGaLocation":43},"/getting-started-videos/","best practice videos",{"text":244,"config":245},"Integrations",{"href":246,"dataGaName":247,"dataGaLocation":43},"/integrations/","integrations",{"title":249,"items":250},"Discover",[251,256,260,265],{"text":252,"config":253},"Customer success stories",{"href":254,"dataGaName":255,"dataGaLocation":43},"/customers/","customer success stories",{"text":257,"config":258},"Blog",{"href":259,"dataGaName":5,"dataGaLocation":43},"/blog/",{"text":261,"config":262},"Remote",{"href":263,"dataGaName":264,"dataGaLocation":43},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":266,"config":267},"TeamOps",{"href":268,"dataGaName":269,"dataGaLocation":43},"/teamops/","teamops",{"title":271,"items":272},"Connect",[273,278,283,288,293],{"text":274,"config":275},"GitLab Services",{"href":276,"dataGaName":277,"dataGaLocation":43},"/services/","services",{"text":279,"config":280},"Community",{"href":281,"dataGaName":282,"dataGaLocation":43},"/community/","community",{"text":284,"config":285},"Forum",{"href":286,"dataGaName":287,"dataGaLocation":43},"https://forum.gitlab.com/","forum",{"text":289,"config":290},"Events",{"href":291,"dataGaName":292,"dataGaLocation":43},"/events/","events",{"text":294,"config":295},"Partners",{"href":296,"dataGaName":297,"dataGaLocation":43},"/partners/","partners",{"backgroundColor":299,"textColor":300,"text":301,"image":302,"link":306},"#2f2a6b","#fff","Insights for the future of software development",{"altText":303,"config":304},"the source promo card",{"src":305},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758208064/dzl0dbift9xdizyelkk4.svg",{"text":307,"config":308},"Read the latest",{"href":309,"dataGaName":310,"dataGaLocation":43},"/the-source/","the source",{"text":312,"config":313,"lists":315},"Company",{"dataNavLevelOne":314},"company",[316],{"items":317},[318,323,329,331,336,341,346,351,356,361,366],{"text":319,"config":320},"About",{"href":321,"dataGaName":322,"dataGaLocation":43},"/company/","about",{"text":324,"config":325,"footerGa":328},"Jobs",{"href":326,"dataGaName":327,"dataGaLocation":43},"/jobs/","jobs",{"dataGaName":327},{"text":289,"config":330},{"href":291,"dataGaName":292,"dataGaLocation":43},{"text":332,"config":333},"Leadership",{"href":334,"dataGaName":335,"dataGaLocation":43},"/company/team/e-group/","leadership",{"text":337,"config":338},"Team",{"href":339,"dataGaName":340,"dataGaLocation":43},"/company/team/","team",{"text":342,"config":343},"Handbook",{"href":344,"dataGaName":345,"dataGaLocation":43},"https://handbook.gitlab.com/","handbook",{"text":347,"config":348},"Investor relations",{"href":349,"dataGaName":350,"dataGaLocation":43},"https://ir.gitlab.com/","investor relations",{"text":352,"config":353},"Trust Center",{"href":354,"dataGaName":355,"dataGaLocation":43},"/security/","trust center",{"text":357,"config":358},"AI Transparency Center",{"href":359,"dataGaName":360,"dataGaLocation":43},"/ai-transparency-center/","ai transparency center",{"text":362,"config":363},"Newsletter",{"href":364,"dataGaName":365,"dataGaLocation":43},"/company/contact/","newsletter",{"text":367,"config":368},"Press",{"href":369,"dataGaName":370,"dataGaLocation":43},"/press/","press",{"text":372,"config":373,"lists":374},"Contact us",{"dataNavLevelOne":314},[375],{"items":376},[377,380,385],{"text":50,"config":378},{"href":52,"dataGaName":379,"dataGaLocation":43},"talk to sales",{"text":381,"config":382},"Support portal",{"href":383,"dataGaName":384,"dataGaLocation":43},"https://support.gitlab.com","support portal",{"text":386,"config":387},"Customer portal",{"href":388,"dataGaName":389,"dataGaLocation":43},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":391,"login":392,"suggestions":399},"Close",{"text":393,"link":394},"To search repositories and projects, login to",{"text":395,"config":396},"gitlab.com",{"href":57,"dataGaName":397,"dataGaLocation":398},"search login","search",{"text":400,"default":401},"Suggestions",[402,404,408,410,414,418],{"text":72,"config":403},{"href":77,"dataGaName":72,"dataGaLocation":398},{"text":405,"config":406},"Code Suggestions (AI)",{"href":407,"dataGaName":405,"dataGaLocation":398},"/solutions/code-suggestions/",{"text":124,"config":409},{"href":126,"dataGaName":124,"dataGaLocation":398},{"text":411,"config":412},"GitLab on AWS",{"href":413,"dataGaName":411,"dataGaLocation":398},"/partners/technology-partners/aws/",{"text":415,"config":416},"GitLab on Google Cloud",{"href":417,"dataGaName":415,"dataGaLocation":398},"/partners/technology-partners/google-cloud-platform/",{"text":419,"config":420},"Why GitLab?",{"href":85,"dataGaName":419,"dataGaLocation":398},{"freeTrial":422,"mobileIcon":427,"desktopIcon":432,"secondaryButton":435},{"text":423,"config":424},"Start free trial",{"href":425,"dataGaName":48,"dataGaLocation":426},"https://gitlab.com/-/trials/new/","nav",{"altText":428,"config":429},"Gitlab Icon",{"src":430,"dataGaName":431,"dataGaLocation":426},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":428,"config":433},{"src":434,"dataGaName":431,"dataGaLocation":426},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":436,"config":437},"Get Started",{"href":438,"dataGaName":439,"dataGaLocation":426},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":441,"mobileIcon":445,"desktopIcon":447},{"text":442,"config":443},"Learn more about GitLab Duo",{"href":77,"dataGaName":444,"dataGaLocation":426},"gitlab duo",{"altText":428,"config":446},{"src":430,"dataGaName":431,"dataGaLocation":426},{"altText":428,"config":448},{"src":434,"dataGaName":431,"dataGaLocation":426},{"freeTrial":450,"mobileIcon":455,"desktopIcon":457},{"text":451,"config":452},"Back to pricing",{"href":204,"dataGaName":453,"dataGaLocation":426,"icon":454},"back to pricing","GoBack",{"altText":428,"config":456},{"src":430,"dataGaName":431,"dataGaLocation":426},{"altText":428,"config":458},{"src":434,"dataGaName":431,"dataGaLocation":426},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":464,"_dir":37,"_draft":6,"_partial":6,"_locale":7,"title":465,"button":466,"image":471,"config":475,"_id":477,"_type":29,"_source":31,"_file":478,"_stem":479,"_extension":34},"/shared/en-us/banner","is now in public beta!",{"text":467,"config":468},"Try the Beta",{"href":469,"dataGaName":470,"dataGaLocation":43},"/gitlab-duo/agent-platform/","duo banner",{"altText":472,"config":473},"GitLab Duo Agent Platform",{"src":474},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1753720689/somrf9zaunk0xlt7ne4x.svg",{"layout":476},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":481,"_dir":37,"_draft":6,"_partial":6,"_locale":7,"data":482,"_id":721,"_type":29,"title":722,"_source":31,"_file":723,"_stem":724,"_extension":34},"/shared/en-us/main-footer",{"text":483,"source":484,"edit":490,"contribute":495,"config":500,"items":505,"minimal":713},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":485,"config":486},"View page source",{"href":487,"dataGaName":488,"dataGaLocation":489},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":491,"config":492},"Edit this page",{"href":493,"dataGaName":494,"dataGaLocation":489},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":496,"config":497},"Please contribute",{"href":498,"dataGaName":499,"dataGaLocation":489},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":501,"facebook":502,"youtube":503,"linkedin":504},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[506,553,606,650,679],{"title":202,"links":507,"subMenu":522},[508,512,517],{"text":509,"config":510},"View plans",{"href":204,"dataGaName":511,"dataGaLocation":489},"view plans",{"text":513,"config":514},"Why Premium?",{"href":515,"dataGaName":516,"dataGaLocation":489},"/pricing/premium/","why premium",{"text":518,"config":519},"Why Ultimate?",{"href":520,"dataGaName":521,"dataGaLocation":489},"/pricing/ultimate/","why ultimate",[523],{"title":524,"links":525},"Contact Us",[526,529,531,533,538,543,548],{"text":527,"config":528},"Contact sales",{"href":52,"dataGaName":53,"dataGaLocation":489},{"text":381,"config":530},{"href":383,"dataGaName":384,"dataGaLocation":489},{"text":386,"config":532},{"href":388,"dataGaName":389,"dataGaLocation":489},{"text":534,"config":535},"Status",{"href":536,"dataGaName":537,"dataGaLocation":489},"https://status.gitlab.com/","status",{"text":539,"config":540},"Terms of use",{"href":541,"dataGaName":542,"dataGaLocation":489},"/terms/","terms of use",{"text":544,"config":545},"Privacy statement",{"href":546,"dataGaName":547,"dataGaLocation":489},"/privacy/","privacy statement",{"text":549,"config":550},"Cookie preferences",{"dataGaName":551,"dataGaLocation":489,"id":552,"isOneTrustButton":106},"cookie preferences","ot-sdk-btn",{"title":105,"links":554,"subMenu":562},[555,559],{"text":556,"config":557},"DevSecOps platform",{"href":70,"dataGaName":558,"dataGaLocation":489},"devsecops platform",{"text":128,"config":560},{"href":77,"dataGaName":561,"dataGaLocation":489},"ai-assisted development",[563],{"title":564,"links":565},"Topics",[566,571,576,581,586,591,596,601],{"text":567,"config":568},"CICD",{"href":569,"dataGaName":570,"dataGaLocation":489},"/topics/ci-cd/","cicd",{"text":572,"config":573},"GitOps",{"href":574,"dataGaName":575,"dataGaLocation":489},"/topics/gitops/","gitops",{"text":577,"config":578},"DevOps",{"href":579,"dataGaName":580,"dataGaLocation":489},"/topics/devops/","devops",{"text":582,"config":583},"Version Control",{"href":584,"dataGaName":585,"dataGaLocation":489},"/topics/version-control/","version control",{"text":587,"config":588},"DevSecOps",{"href":589,"dataGaName":590,"dataGaLocation":489},"/topics/devsecops/","devsecops",{"text":592,"config":593},"Cloud Native",{"href":594,"dataGaName":595,"dataGaLocation":489},"/topics/cloud-native/","cloud native",{"text":597,"config":598},"AI for Coding",{"href":599,"dataGaName":600,"dataGaLocation":489},"/topics/devops/ai-for-coding/","ai for coding",{"text":602,"config":603},"Agentic AI",{"href":604,"dataGaName":605,"dataGaLocation":489},"/topics/agentic-ai/","agentic ai",{"title":607,"links":608},"Solutions",[609,611,613,618,622,625,629,632,634,637,640,645],{"text":149,"config":610},{"href":144,"dataGaName":149,"dataGaLocation":489},{"text":138,"config":612},{"href":120,"dataGaName":121,"dataGaLocation":489},{"text":614,"config":615},"Agile development",{"href":616,"dataGaName":617,"dataGaLocation":489},"/solutions/agile-delivery/","agile delivery",{"text":619,"config":620},"SCM",{"href":134,"dataGaName":621,"dataGaLocation":489},"source code management",{"text":567,"config":623},{"href":126,"dataGaName":624,"dataGaLocation":489},"continuous integration & delivery",{"text":626,"config":627},"Value stream management",{"href":177,"dataGaName":628,"dataGaLocation":489},"value stream management",{"text":572,"config":630},{"href":631,"dataGaName":575,"dataGaLocation":489},"/solutions/gitops/",{"text":187,"config":633},{"href":189,"dataGaName":190,"dataGaLocation":489},{"text":635,"config":636},"Small business",{"href":194,"dataGaName":195,"dataGaLocation":489},{"text":638,"config":639},"Public sector",{"href":199,"dataGaName":200,"dataGaLocation":489},{"text":641,"config":642},"Education",{"href":643,"dataGaName":644,"dataGaLocation":489},"/solutions/education/","education",{"text":646,"config":647},"Financial services",{"href":648,"dataGaName":649,"dataGaLocation":489},"/solutions/finance/","financial services",{"title":207,"links":651},[652,654,656,658,661,663,665,667,669,671,673,675,677],{"text":219,"config":653},{"href":221,"dataGaName":222,"dataGaLocation":489},{"text":224,"config":655},{"href":226,"dataGaName":227,"dataGaLocation":489},{"text":229,"config":657},{"href":231,"dataGaName":232,"dataGaLocation":489},{"text":234,"config":659},{"href":236,"dataGaName":660,"dataGaLocation":489},"docs",{"text":257,"config":662},{"href":259,"dataGaName":5,"dataGaLocation":489},{"text":252,"config":664},{"href":254,"dataGaName":255,"dataGaLocation":489},{"text":261,"config":666},{"href":263,"dataGaName":264,"dataGaLocation":489},{"text":274,"config":668},{"href":276,"dataGaName":277,"dataGaLocation":489},{"text":266,"config":670},{"href":268,"dataGaName":269,"dataGaLocation":489},{"text":279,"config":672},{"href":281,"dataGaName":282,"dataGaLocation":489},{"text":284,"config":674},{"href":286,"dataGaName":287,"dataGaLocation":489},{"text":289,"config":676},{"href":291,"dataGaName":292,"dataGaLocation":489},{"text":294,"config":678},{"href":296,"dataGaName":297,"dataGaLocation":489},{"title":312,"links":680},[681,683,685,687,689,691,693,697,702,704,706,708],{"text":319,"config":682},{"href":321,"dataGaName":314,"dataGaLocation":489},{"text":324,"config":684},{"href":326,"dataGaName":327,"dataGaLocation":489},{"text":332,"config":686},{"href":334,"dataGaName":335,"dataGaLocation":489},{"text":337,"config":688},{"href":339,"dataGaName":340,"dataGaLocation":489},{"text":342,"config":690},{"href":344,"dataGaName":345,"dataGaLocation":489},{"text":347,"config":692},{"href":349,"dataGaName":350,"dataGaLocation":489},{"text":694,"config":695},"Sustainability",{"href":696,"dataGaName":694,"dataGaLocation":489},"/sustainability/",{"text":698,"config":699},"Diversity, inclusion and belonging (DIB)",{"href":700,"dataGaName":701,"dataGaLocation":489},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":352,"config":703},{"href":354,"dataGaName":355,"dataGaLocation":489},{"text":362,"config":705},{"href":364,"dataGaName":365,"dataGaLocation":489},{"text":367,"config":707},{"href":369,"dataGaName":370,"dataGaLocation":489},{"text":709,"config":710},"Modern Slavery Transparency Statement",{"href":711,"dataGaName":712,"dataGaLocation":489},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":714},[715,717,719],{"text":539,"config":716},{"href":541,"dataGaName":542,"dataGaLocation":489},{"text":544,"config":718},{"href":546,"dataGaName":547,"dataGaLocation":489},{"text":549,"config":720},{"dataGaName":551,"dataGaLocation":489,"id":552,"isOneTrustButton":106},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[726],{"_path":727,"_dir":728,"_draft":6,"_partial":6,"_locale":7,"content":729,"config":733,"_id":735,"_type":29,"title":18,"_source":31,"_file":736,"_stem":737,"_extension":34},"/en-us/blog/authors/dylan-griffith","authors",{"name":18,"config":730},{"headshot":731,"ctfId":732},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749672822/Blog/Author%20Headshots/DylanGriffith-headshot.jpg","DylanGriffith",{"template":734},"BlogAuthor","content:en-us:blog:authors:dylan-griffith.yml","en-us/blog/authors/dylan-griffith.yml","en-us/blog/authors/dylan-griffith",{"_path":739,"_dir":37,"_draft":6,"_partial":6,"_locale":7,"header":740,"eyebrow":741,"blurb":742,"button":743,"secondaryButton":747,"_id":749,"_type":29,"title":750,"_source":31,"_file":751,"_stem":752,"_extension":34},"/shared/en-us/next-steps","Start shipping better software faster","50%+ of the Fortune 100 trust GitLab","See what your team can do with the intelligent\n\n\nDevSecOps platform.\n",{"text":45,"config":744},{"href":745,"dataGaName":48,"dataGaLocation":746},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":50,"config":748},{"href":52,"dataGaName":53,"dataGaLocation":746},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":754,"content":755,"config":758,"_id":28,"_type":29,"title":30,"_source":31,"_file":32,"_stem":33,"_extension":34},{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},{"title":9,"description":10,"authors":756,"heroImage":11,"date":19,"body":20,"category":21,"tags":757},[18],[23,24],{"slug":26,"featured":6,"template":27},1761814425620]