[{"data":1,"prerenderedAt":700},["ShallowReactive",2],{"/en-us/blog/moving-all-your-data/":3,"navigation-en-us":32,"banner-en-us":449,"footer-en-us":461,"Jacob Vosmaer":672,"next-steps-en-us":685},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":22,"_id":25,"_type":26,"title":27,"_source":28,"_file":29,"_stem":30,"_extension":31},"/en-us/blog/moving-all-your-data","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Moving all your data, 9TB edition","At GitLab B.V. we are working on an infrastructure upgrade to give more CPU power and storage space to GitLab.com. Learn more here!","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749684774/Blog/Hero%20Images/van.jpg","https://about.gitlab.com/blog/moving-all-your-data","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"Moving all your data, 9TB edition\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Jacob Vosmaer\"}],\n        \"datePublished\": \"2015-03-09\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21},[18],"Jacob Vosmaer","2015-03-09","\nAt GitLab B.V. we are working on an infrastructure upgrade to give more CPU\npower and storage space to GitLab.com. (We are currently still running on a\n[single server](/blog/the-hardware-that-powers-100k-git-repos/).) As a\npart of this upgrade we wanted to move gitlab.com from our own dedicated\nhardware servers to an AWS data center 400 kilometers away.  In this blog post\nI will tell you how I did that and what challenges I had to overcome. An epic\nadventure of hand-rolled network tunnels, advanced DRBD features and streaming\n9TB of data through SSH pipes!\n\n\u003C!-- more -->\n\n## What did I have to move?\n\nIn our current setup we run a stock GitLab Enterprise Edition omnibus package,\nwith a single big filesystem mounted at `/var/opt/gitlab`. This\nfilesystem holds all the user data hosted on gitlab.com: Postgres and Redis\ndatabase files, user uploads, and a lot of Git repositories. All I had to do\nto move this data to AWS is to move the files on this filesystem. Sounds simple\nenough, does it not?\n\nSo do we move the files, or the filesystem itself? This is an easy question to\nanswer. Moving the files using something like Rsync is not an option because it\nis just too slow. We do file-based backups every week where we take a block\ndevice snapshot, mount the snapshot and send it across with Rsync. That\ncurrently takes over 24 hours, and 24 hours of downtime while we move\ngitlab.com is not a nice idea. Now you might ask: what if you Rsync once to\nprepare, take the server offline, and then do a quick Rsync just to catch up?\nThat would still take hours just for Rsync to walk through all the files and\ndirectories on disk. No good.\n\nWe have faced and solved this same problem in the past when the amount of data\nwas 5 times smaller. (Rsync was not an option even then.) What I did at that\ntime was to use DRBD to move not just the files themselves, but the whole\nfilesystem they sit on. This time around DRBD again seemed like the best\nsolution for us. It is not the fastest solution to move a lot of data, but what\nis great about it is that you can keep using the filesystem while the data is\nbeing moved, and changes will get synchronized continuously. No downtime for\nour users! (Except maybe 5 minutes at the start to set up the sync.)\n\n## What is DRBD?\n\n[DRBD](http://www.drbd.org) is a system that can create a virtual hard drive\n(block device) on a Linux computer that gets mirrored across a network\nconnection to a second Linux computer. Both computers give a 'real' hard drive\nto DRBD, and DRBD keeps the contents of the real hard drive the same across\nboth computers via the network. One of the two computers gets a virtual hard\ndrive from DRBD, which shows the contents of the real hard drive underneath. If\nyour first computer crashes, you can 'plug in' the virtual hard drive on the\nsecond computer in a matter of seconds, and all your data will still be there\nbecause DRBD kept the 'real' hard drives in sync for you. You can even have the\ntwo computers that are linked by DRBD sit in different buildings, or on\ndifferent continents. Up until our move to AWS, we were using DRBD to protect\nagainst hardware failure on the server that runs gitlab.com: if such a failure\nwould happen, we could just plug in the virtual hard drive with the user data\ninto our stand-by server. In our new data center, the hosting provider (Amazon\nWeb Services) has their own solution for plugging virtual hard drives in and\nout called Elastic Block Storage, so we are no longer using DRBD as a virtual\nhard drive. From an availability standpoint this is not better or worse, but\nusing EBS drives does make it a lot easier for us to make backups because now\nwe can just store snapshots (no more Rsync).\n\n## Using DRBD for a data migration\n\nAlthough DRBD is not really made for this purpose, I felt confident using DRBD\nfor the migration because I had done it before for a migration between data\ncenters. At that time we were moving across the Atlantic Ocean; this time we\nwould only be moving from the Netherlands to Germany.  However, the last time\nwe used DRBD only as a one-off tool. In our pre-migration setup, we were\nalready using DRBD to replicate the filesystem between two servers in the same\nrack. DRBD only lets you share a virtual hard drive between two computers, so\nhow do we now send the data to a _third_ computer in the new data center?\n\nLuckily, DRBD actually has a trick up its sleeve to deal with this, called\n'stacked resources'. This means that our old servers ('linus' and 'monty')\nwould share a virtual hard drive called 'drbd0', and that whoever of the two\nhas the 'drbd0' virtual hard drive plugged in gets to use 'drbd0' as the 'real'\nhard drive underneath a second virtual hard drive, called 'drbd10', which is\nshared with the new server ('theo'). Also see the picture below.\n\n![Stacked DRBD replication](https://about.gitlab.com/images/drbd/drbd-three-nodes.png)\n\nIf linus would malfunction, we could attach drbd0 (the blue virtual hard drive)\non monty and keep gitlab.com going. The 'green' replication (to get the data to\ntheo) would also be able to continue, even after a failover to monty.\n\n## Networking\n\nI liked the picture above, so 'all' I had to do was set it up. That ended up\ntaking a few days, just to set up a test environment, and to figure out how to\ncreate a network tunnel for the green traffic. The network tunnel needed to\nhave a movable endpoint depending on whether linus or monty was primary. We\nalso needed the tunnel because DRBD is not compatible with the [Network Address\nTranslation](http://en.wikipedia.org/wiki/Network_address_translation) used by\nAWS. DRBD assumes that whenever a node listens on an IP address, it is also\nreachable for its partner node at that IP address. On AWS on the other hand, a\nnode will have one or more internal IP addresses, which are distinct from its\n_public_ IP address.\n\nWe chose to work around this with an [IPIP\ntunnel](http://en.wikipedia.org/wiki/IP_in_IP) and manually keyed IPsec\nencryption. Previous experiments indicated that this gave us the best network\nthroughput compared to OpenVPN and GRE tunnels.\n\nTo set up the tunnel I used a shell script that was kept in sync on all three\nservers involved in the migration by Chef.\n\n```\n# Network tunnel configuration script used by GitLab B.V. to migrate data from\n# Delft to Frankfurt\n\n#!/bin/sh\nset -u\n\nPATH=/usr/sbin:/sbin:/usr/bin:/bin\n\nfrankfurt_public=54.93.71.23\nfrankfurt_replication=172.16.228.2\ntest_public=54.152.127.180\ntest_replication=172.16.228.1\ndelft_public=62.204.93.103\ndelft_replication=172.16.228.1\n\ncreate_tunipip() {\n  if ! ip tunnel show | grep -q tunIPIP ; then\n    echo Creating tunnel tunIPIP\n    ip tunnel add tunIPIP mode ipip ttl 64 local \"$1\" remote \"$2\"\n  fi\n}\n\nadd_tunnel_address() {\n  if ! ip address show tunIPIP | grep -q \"$1\" ; then\n    ip address add \"$1/32\" peer \"$2/32\" dev tunIPIP\n  fi\n}\n\ncase $(hostname) in\n  ip-10-0-2-9)\n    create_tunipip 10.0.2.140 \"${frankfurt_public}\"\n    add_tunnel_address \"${test_replication}\" \"${frankfurt_replication}\"\n    ip link set tunIPIP up\n    ;;\n  ip-10-0-2-245)\n    create_tunipip 10.0.2.11 \"${frankfurt_public}\"\n    add_tunnel_address \"${test_replication}\" \"${frankfurt_replication}\"\n    ip link set tunIPIP up\n    ;;\n  ip-10-1-0-52|theo.gitlab.com)\n    create_tunipip 10.1.0.52 \"${delft_public}\"\n    add_tunnel_address \"${frankfurt_replication}\" \"${delft_replication}\"\n    ip link set tunIPIP up\n    ;;\n  linus|monty)\n    create_tunipip \"${delft_public}\" \"${frankfurt_public}\"\n    add_tunnel_address \"${delft_replication}\" \"${frankfurt_replication}\"\n    ip link set tunIPIP up\n    ;;\nesac\n```\n\nThis script was configured to run on boot. Note that it covers our Delft nodes\n(linus and monty, then current production), the node we were migrating to in\nFrankfurt (theo), and two AWS test nodes that were part of a staging setup. We\nchose the AWS Frankfurt (Germany) data center because of its geographic\nproximity to Delft (The Netherlands).\n\nWe configured IPsec with `/etc/ipsec-tools.conf`. An example for the 'origin'\nconfiguration would be:\n\n```\n#!/usr/sbin/setkey -f\n\n# Configuration for 172.16.228.1\n\n# Flush the SAD and SPD\nflush;\nspdflush;\n\n# Attention: Use this keys only for testing purposes!\n# Generate your own keys!\n\n# AH SAs using 128 bit long keys\n# Fill in your keys below!\nadd 172.16.228.1 172.16.228.2 ah 0x200 -A hmac-md5 0xfoobar;\nadd 172.16.228.2 172.16.228.1 ah 0x300 -A hmac-md5 0xbarbaz;\n\n# ESP SAs using 192 bit long keys (168 + 24 parity)\n# Fill in your keys below!\nadd 172.16.228.1 172.16.228.2 esp 0x201 -E 3des-cbc 0xquxfoo;\nadd 172.16.228.2 172.16.228.1 esp 0x301 -E 3des-cbc 0xbazqux;\n\n# Security policies\n# outbound traffic from 172.16.228.1 to 172.16.228.2\nspdadd 172.16.228.1 172.16.228.2 any -P out ipsec esp/transport//require ah/transport//require;\n\n# inbound traffic from 172.16.228.2 to 172.16.228.1\nspdadd 172.16.228.2 172.16.228.1 any -P in ipsec esp/transport//require ah/transport//require;\n```\n\nGetting the networking to this point took quite some work. For starters, we did\nnot have a staging environment similar enough to our production environment, so\nI had to create one for this occasion.\n\nOn top of that, to model our production setup, I had to use an AWS 'Virtual\nPrivate Cloud', which was new technology for us. It took a while before I\nfound some [vital information about using multiple IP\naddresses](http://engineering.silk.co/post/31923247961/multiple-ip-addresses-on-amazon-ec2)\nthat was not obvious from the AWS documentation: if you want to have two public\nIP addresses on an AWS VPC node, you need to put two corresponding private IP\naddresses on one 'Elastic Network Interface', instead of creating two network\ninterfaces with one private IP each.\n\n## Configuring three-way DRBD replication\n\nWith the basic networking figured out the next thing I had to do was to adapt\nour production failover script so that we maintain redundancy while migrating\nthe data. 'Failover' is a procedure where you move a service (gitlab.com) ove\nto a different computer after a failure. Our failover procedure is managed by a\nscript. My goal was to make sure that if one of our production servers failed,\nany teammate of mine on pager duty would be able to restore the gitlab.com\nservice using our normal failover procedure. That meant I had to update the\nscript to use the new three-way DRBD configuration.\n\nI certainly got a little more familiar with tcpdump (`tcpdump -n -i\nINTERFACE`), having multiple layers of firewalls\n([UFW](http://en.wikipedia.org/wiki/Uncomplicated_Firewall) and AWS [Security\nGroups](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html)),\nand how to get any useful log messages from DRBD:\n\n```\n# Monitor DRBD log messages\nsudo tail -f /var/log/messages | grep -e drbd -e d-con\n```\n\nI later learned that I actually deployed a new version of the failover script\nwith a bug in it that potentially could have confused the hell out of my\nteammates had they had to use it under duress. Luckily we never actually needed\nthe failover procedure during the time the new script was in production.\n\nBut, even though I was introducing new complexity and hence bugs into our\nfailover tooling, I did manage to learn and try out enough things to bring this\nproject to a successful conclusion.\n\n## Enabling the DRBD replication\n\nThis part was relatively easy. I just had to grow the DRBD block device\n'drbd0' so that it could accommodate the new stacked (inner) block device\n'drbd10' without having to shrink our production filesystem. Because drbd0 was\nbacked by LVM and we had some space left this was a matter of invoking\n`lvextend` and `drbdadm resize` on both our production nodes.\n\nThe step after this was the first one where I had to take gitlab.com offline.\nIn order to 'activate' drbd10 and start the synchronization, I had to unmount\n`/dev/drbd0` from `/var/opt/gitlab` and mount `/dev/drbd10` in its place. This\ntook less than 5 minutes. After this the actual migration was under way!\n\n## Too slow\n\nAt this point I was briefly excited to be able to share some good news with the\nrest of the team. While staring about the DRBD progress bar for the\nsynchronization I started to realize however that the progress bar was telling\nme that the synchronization would take about 50-60 days at 2MB/s.\n\nThis prognosis was an improvement over what we would expect based on our\nprevious experience moving 1.8TB from North Virginia (US) to Delft (NL) in\nabout two weeks (across the Atlantic Ocean!). If one would extrapolate that\nrate you would expect moving 9TB to take 70 days. We were disappointed\nnonetheless because we were hoping that we would gain more throughput by moving\nover a shorter distance this time around (Delft and Frankfurt are about 400km\napart).\n\nThe first thing I started looking into at this point was whether we could\nsomehow make better use of the network bandwidth at our disposal. Sending fake\ndata (zeroes) over the (encrypted) IPIP tunnel (`dd if=/dev/zero | nc remote_ip\n1234`) we could get about 17 MB/s. By disabling IPsec (not really an option as\nfar as I am concerned) we could increase that number to 40 MB/s.\n\nThe only conclusion I could come to was that we were not reaching our maximum\nbandwidth potential, but that I had no clue how to coax more speed out of the\nDRBD sync. Luckily I recalled reading about another magical DRBD feature.\n\n## Bring out the truck\n\nThe solution suggested by the DRBD documentation for situations like ours is\ncalled ['truck based\nreplication](https://drbd.linbit.com/users-guide/s-using-truck-based-replication.html).\nInstead of synchronizing 9TB of data, we would be telling DRBD to mark a point\nin time, take a full disk snapshot, move the snapshot to the new location (as a\nbox full of hard drives in a truck if needed), and then tell DRBD to get the\ndata at the new location up to date. During that 'catching-up' sync, DRBD would\nonly be resending those parts of the disk that actually changed since we marked\nthe point in time earlier. Because our users would not have written 9TB of new\ndata while the 'disks' were being shipped, we would have to sync much less than\n9TB.\n\n![Full replication versus 'truck' replication](https://about.gitlab.com/images/drbd/drbd-truck-sync.png)\n\nIn our case I would not have to use an actual truck; while testing the network\nthroughput between our old and new server I found that I could stream zeroes\nthrough SSH at about 35MB/s.\n\n```\ndd if=/dev/zero bs=1M count=100 | ssh theo.gitlab.com dd of=/dev/null\n```\n\nAfter doing some testing with the leftover two-node staging setup I built\nearlier to figure out the networking I felt I could make this work. I followed\nthe steps in the DRBD documentation, made an LVM snapshot on the active origin\nserver, and started sending the snapshot to the new server with the following\nscript.\n\n```\n#!/bin/sh\nblock_count=100\nblock_size='8M'\nremote='54.93.71.23'\n\nsend_blocks() {\n  for skip in $(seq $1 ${block_count} $2) ; do\n    echo \"${skip}   $(date)\"\n    sudo dd if=/dev/gitlab_vg/truck bs=${block_size} count=${block_count} skip=${skip} status=noxfer iflag=fullblock \\\n    | ssh -T ${remote} sudo dd of=/dev/gitlab_vg/gitlab_com bs=${block_size} count=${block_count} seek=${skip} status=none iflag=fullblock\n  done\n}\n\ncheck_blocks() {\n  for skip in $(seq $2 ${block_count} $3) ; do\n    printf \"${skip}   \"\n    sudo dd if=$1 bs=${block_size} count=${block_count} skip=${skip} iflag=fullblock | md5sum\n  done\n}\n\ncase $1 in\n  send)\n    send_blocks $2 $3\n    ;;\n  check)\n    check_blocks $2 $3 $4\n    ;;\n  *)\n    echo \"Usage: $0 (send START END) | (check BLOCK_DEVICE START END)\"\n    exit 127\nesac\n```\n\nBy running this script in a [screen](http://www.gnu.org/software/screen/)\nsession I was able to copy the LVM snapshot `/dev/gitlab_vg/truck` from the old\nserver to the new server in about 3.5 days, 800 MB at a time. The 800MB number\nwas a bit of a coincidence, stemming from the recommendation from our Dutch\nhosters [NetCompany](http://www.netcompany.nl/) to use 8MB `dd`-blocks. Also\ncoincidentally, the total disk size was divisible by 8MB. If you have an eye\nfor system security you might notice that the script needed both root\nprivileges on the source server, and via short-lived unattended SSH sessions\ninto the remote server (`| ssh sudo ...`). This is not a normal thing for us to\ndo, and my colleagues got spammed by warning messages about it while this\nmigration was in progress.\n\nBecause I am a little paranoid, I was running a second instance of this script\nin parallel with the sync, where I was calculating MD5 checksums of all the\nblocks that were being sent across the network. By calculating the same\nchecksums on the migration target I could gain sufficient confidence that all\ndata made across without errors. If there would have been any, the script would\nhave made it easy to re-send an individual 800MB block.\n\nAt this point my spirits were lifting again and I told my teammates we would\nprobably need one extra day after the 'truck' stage before we could start using\nthe new server. I did not know yet that 'one day' would become 'one week'.\n\n## Shipping too much data\n\nAfter moving the big snapshot across the network with\n[dd](http://en.wikipedia.org/wiki/Dd_%28Unix%29) and SSH, the next step would\nbe to 'just turn DRBD on and let it catch up'. But that did not work all of a\nsudden! It took me a while to realize that the problem was that while trucking,\nI had sent _too much_ data to the new server (theo). If you recall the picture\nI drew earlier of the three-way DRBD replication then you can see that the goal\nwas to replicate the 'green box' from the old servers to the new server, while\nletting the old servers keep sharing the 'blue box' for redundancy.\n\n![Blue box on the left, green box on the\nright](https://about.gitlab.com/images/drbd/drbd-too-much-data.png)\n\nBut I had just sent a snapshot of the _blue_ box to theo (the server on the\nright), not just the green box. DRBD was refusing to turn back on theo,\nbecause it was expecting the green box, not the blue box (containing the green\nbox). More precisely, my disk on the new server contained metadata for drbd0 as\nwell as drbd10. DRBD finds its metadata by starting at the end of the disk and\nwalking backwards. Because of that, it was not seeing the drbd10 (green)\nmetadata on theo.\n\n![Two metadata block](https://about.gitlab.com/images/drbd/drbd-two-metadata-blocks.png)\n\nThe first thing I tried was to shrink the disk (with\n[LVM](http://en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29)) so that\nthe blue block at the end would fall off. Unfortunately, you can only grow and\nshrink LVM disks in fixed steps (4MB steps in our case), and those steps did\nnot align with where the drbd10 metadata (green box) ended on disk.\n\nThe next thing I tried was to erase the blue block. That would leave DRBD\nunable to find any metadata, because DRBD metadata must sit at the end of the\ndisk. To cope with that I tried and trick DRBD into thinking it was in the\nmiddle of a disk resize operation. By manually creating a doctored\n`/var/lib/drbd/drbd-minor-10.lkbd` file used by DRBD when it does a\n(legitimate) disk resize, I was pointing it to where I thought it could find\nthe green block of drbd10 metadata. To be honest this required more disk sector\narithmetic than I was comfortable with. Comfortable or not, I never got this\nprocedure to work without a few screens full of scary DRBD error messages so I\ndecided to call our first truck expedition a bust.\n\n## One last try\n\nWe had just spent four days waiting for a 9TB chunk of data to be transported\nto our new server only to find out that it was getting rejected by DRBD. The\nonly option that seemed left to us was to sit back and wait 50-60 days for a\nregular DRBD sync to happen. There was just this one last thing I wanted to try\nbefore giving up. The stumbling block at this point was getting DRBD on theo to\nfind the metadata for the drbd10 disk. From reading the documentation, I knew\nthat DRBD has metadata export and import commands. What if we would take a new\nLVM snapshot in Delft, take the disk offline and export its metadata, and then\non the other hand do a metadata import with the proper DRBD import command\n(instead of me writing zeroes to the disk and lying to DRBD about being in the\nmiddle of a resize). This would require us to redo the truck dance and wait\nfour days, but four days was still better than 50 days.\n\nUsing the staging setup I built at the start of this process (a good time\ninvestment!) I created a setup that allowed me to test three-way replication\nand truck-based replication at the same time. Without having to do any\narithmetic I came up with an intimidating but reliable sequence of commands to\n(1) initiate truck based replication and (2) export the DRBD metadata.\n\n```\nsudo lvremove -f gitlab_vg/truck\n## clear the bitmap to mark the sync point in time\nsudo drbdadm disconnect --stacked gitlab_data-stacked\nsudo drbdadm new-current-uuid --clear-bitmap --stacked gitlab_data-stacked/0\n## create a metadata dump\necho Yes | sudo gitlab-drbd slave\nsudo drbdadm primary gitlab_data\nsudo drbdadm apply-al --stacked gitlab_data-stacked\nsudo drbdadm dump-md --stacked gitlab_data-stacked > stacked-md-$(date +%s).txt\n## Create a block device snapshot\nsudo lvcreate -n truck -s --extents 50%FREE gitlab_vg/drbd\n## Turn gitlab back on\necho Yes |sudo gitlab-drbd slave\necho Yes |sudo gitlab-drbd master\n## Make sure the current node will 'win' as primary later on\nsudo drbdadm new-current-uuid --stacked gitlab_data-stacked/0\n```\n\nThis time I needed to take gitlab.com offline for a few minutes to be able to\ndo the metadata export. After that, a second waiting period of 4 days of\nstreaming the disk snapshot with `dd` and `ssh` commenced. And then came the\nbig moment of turning DRBD back on theo. It worked! Now I just had to wait\nfor the changes on disk of the last four days to be replicated (which took\nabout a day) and we were ready to flip the big switch, update the DNS and start\nserving gitlab.com from AWS. That final transition took another 10 minutes of\ndowntime, and then we were done.\n\n## Looking back\n\nAs soon as we flipped the switch and started operating out of AWS/Frankfurt,\ngitlab.com became noticeably more responsive. This is in spite of the fact that\nwe are _still_ running on a single server (an [AWS\nc3.8xlarge](http://aws.amazon.com/ec2/instance-types/#c3) instance at the\nmoment).\n\nCounting from the moment I was tasked to work on this data migration, we were\nable to move a 9TB filesystem to a different data center and hosting provider\nin three weeks, requiring 20 minutes of total downtime (spread over three\nmaintenance windows). We took an operational risk of prolonged downtime due to\noperator confusion in case of incidents, by deploying a new configuration that\nwhile tested to some degree was understood by only one member of the operations\nteam (myself). We were lucky that there was no incident during those three\nweeks that made this lack of shared knowledge a problem.\n\nNow if you will excuse me I have to go and explain to my colleagues how our\nnew gitlab.com infrastructure on AWS is set up. :)\n","engineering",{"slug":23,"featured":6,"template":24},"moving-all-your-data","BlogPost","content:en-us:blog:moving-all-your-data.yml","yaml","Moving All Your Data","content","en-us/blog/moving-all-your-data.yml","en-us/blog/moving-all-your-data","yml",{"_path":33,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"data":35,"_id":445,"_type":26,"title":446,"_source":28,"_file":447,"_stem":448,"_extension":31},"/shared/en-us/main-navigation","en-us",{"logo":36,"freeTrial":41,"sales":46,"login":51,"items":56,"search":386,"minimal":417,"duo":436},{"config":37},{"href":38,"dataGaName":39,"dataGaLocation":40},"/","gitlab logo","header",{"text":42,"config":43},"Get free trial",{"href":44,"dataGaName":45,"dataGaLocation":40},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":47,"config":48},"Talk to sales",{"href":49,"dataGaName":50,"dataGaLocation":40},"/sales/","sales",{"text":52,"config":53},"Sign in",{"href":54,"dataGaName":55,"dataGaLocation":40},"https://gitlab.com/users/sign_in/","sign in",[57,101,197,202,307,367],{"text":58,"config":59,"cards":61,"footer":84},"Platform",{"dataNavLevelOne":60},"platform",[62,68,76],{"title":58,"description":63,"link":64},"The most comprehensive AI-powered DevSecOps Platform",{"text":65,"config":66},"Explore our Platform",{"href":67,"dataGaName":60,"dataGaLocation":40},"/platform/",{"title":69,"description":70,"link":71},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":72,"config":73},"Meet GitLab Duo",{"href":74,"dataGaName":75,"dataGaLocation":40},"/gitlab-duo/","gitlab duo ai",{"title":77,"description":78,"link":79},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":80,"config":81},"Learn more",{"href":82,"dataGaName":83,"dataGaLocation":40},"/why-gitlab/","why gitlab",{"title":85,"items":86},"Get started with",[87,92,97],{"text":88,"config":89},"Platform Engineering",{"href":90,"dataGaName":91,"dataGaLocation":40},"/solutions/platform-engineering/","platform engineering",{"text":93,"config":94},"Developer Experience",{"href":95,"dataGaName":96,"dataGaLocation":40},"/developer-experience/","Developer experience",{"text":98,"config":99},"MLOps",{"href":100,"dataGaName":98,"dataGaLocation":40},"/topics/devops/the-role-of-ai-in-devops/",{"text":102,"left":103,"config":104,"link":106,"lists":110,"footer":179},"Product",true,{"dataNavLevelOne":105},"solutions",{"text":107,"config":108},"View all Solutions",{"href":109,"dataGaName":105,"dataGaLocation":40},"/solutions/",[111,136,158],{"title":112,"description":113,"link":114,"items":119},"Automation","CI/CD and automation to accelerate deployment",{"config":115},{"icon":116,"href":117,"dataGaName":118,"dataGaLocation":40},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[120,124,128,132],{"text":121,"config":122},"CI/CD",{"href":123,"dataGaLocation":40,"dataGaName":121},"/solutions/continuous-integration/",{"text":125,"config":126},"AI-Assisted Development",{"href":74,"dataGaLocation":40,"dataGaName":127},"AI assisted development",{"text":129,"config":130},"Source Code Management",{"href":131,"dataGaLocation":40,"dataGaName":129},"/solutions/source-code-management/",{"text":133,"config":134},"Automated Software Delivery",{"href":117,"dataGaLocation":40,"dataGaName":135},"Automated software delivery",{"title":137,"description":138,"link":139,"items":144},"Security","Deliver code faster without compromising security",{"config":140},{"href":141,"dataGaName":142,"dataGaLocation":40,"icon":143},"/solutions/security-compliance/","security and compliance","ShieldCheckLight",[145,148,153],{"text":146,"config":147},"Security & Compliance",{"href":141,"dataGaLocation":40,"dataGaName":146},{"text":149,"config":150},"Software Supply Chain Security",{"href":151,"dataGaLocation":40,"dataGaName":152},"/solutions/supply-chain/","Software supply chain security",{"text":154,"config":155},"Compliance & Governance",{"href":156,"dataGaLocation":40,"dataGaName":157},"/solutions/continuous-software-compliance/","Compliance and governance",{"title":159,"link":160,"items":165},"Measurement",{"config":161},{"icon":162,"href":163,"dataGaName":164,"dataGaLocation":40},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[166,170,174],{"text":167,"config":168},"Visibility & Measurement",{"href":163,"dataGaLocation":40,"dataGaName":169},"Visibility and Measurement",{"text":171,"config":172},"Value Stream Management",{"href":173,"dataGaLocation":40,"dataGaName":171},"/solutions/value-stream-management/",{"text":175,"config":176},"Analytics & Insights",{"href":177,"dataGaLocation":40,"dataGaName":178},"/solutions/analytics-and-insights/","Analytics and insights",{"title":180,"items":181},"GitLab for",[182,187,192],{"text":183,"config":184},"Enterprise",{"href":185,"dataGaLocation":40,"dataGaName":186},"/enterprise/","enterprise",{"text":188,"config":189},"Small Business",{"href":190,"dataGaLocation":40,"dataGaName":191},"/small-business/","small business",{"text":193,"config":194},"Public Sector",{"href":195,"dataGaLocation":40,"dataGaName":196},"/solutions/public-sector/","public sector",{"text":198,"config":199},"Pricing",{"href":200,"dataGaName":201,"dataGaLocation":40,"dataNavLevelOne":201},"/pricing/","pricing",{"text":203,"config":204,"link":206,"lists":210,"feature":294},"Resources",{"dataNavLevelOne":205},"resources",{"text":207,"config":208},"View all resources",{"href":209,"dataGaName":205,"dataGaLocation":40},"/resources/",[211,244,266],{"title":212,"items":213},"Getting started",[214,219,224,229,234,239],{"text":215,"config":216},"Install",{"href":217,"dataGaName":218,"dataGaLocation":40},"/install/","install",{"text":220,"config":221},"Quick start guides",{"href":222,"dataGaName":223,"dataGaLocation":40},"/get-started/","quick setup checklists",{"text":225,"config":226},"Learn",{"href":227,"dataGaLocation":40,"dataGaName":228},"https://university.gitlab.com/","learn",{"text":230,"config":231},"Product documentation",{"href":232,"dataGaName":233,"dataGaLocation":40},"https://docs.gitlab.com/","product documentation",{"text":235,"config":236},"Best practice videos",{"href":237,"dataGaName":238,"dataGaLocation":40},"/getting-started-videos/","best practice videos",{"text":240,"config":241},"Integrations",{"href":242,"dataGaName":243,"dataGaLocation":40},"/integrations/","integrations",{"title":245,"items":246},"Discover",[247,252,256,261],{"text":248,"config":249},"Customer success stories",{"href":250,"dataGaName":251,"dataGaLocation":40},"/customers/","customer success stories",{"text":253,"config":254},"Blog",{"href":255,"dataGaName":5,"dataGaLocation":40},"/blog/",{"text":257,"config":258},"Remote",{"href":259,"dataGaName":260,"dataGaLocation":40},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":262,"config":263},"TeamOps",{"href":264,"dataGaName":265,"dataGaLocation":40},"/teamops/","teamops",{"title":267,"items":268},"Connect",[269,274,279,284,289],{"text":270,"config":271},"GitLab Services",{"href":272,"dataGaName":273,"dataGaLocation":40},"/services/","services",{"text":275,"config":276},"Community",{"href":277,"dataGaName":278,"dataGaLocation":40},"/community/","community",{"text":280,"config":281},"Forum",{"href":282,"dataGaName":283,"dataGaLocation":40},"https://forum.gitlab.com/","forum",{"text":285,"config":286},"Events",{"href":287,"dataGaName":288,"dataGaLocation":40},"/events/","events",{"text":290,"config":291},"Partners",{"href":292,"dataGaName":293,"dataGaLocation":40},"/partners/","partners",{"backgroundColor":295,"textColor":296,"text":297,"image":298,"link":302},"#2f2a6b","#fff","Insights for the future of software development",{"altText":299,"config":300},"the source promo card",{"src":301},"/images/navigation/the-source-promo-card.svg",{"text":303,"config":304},"Read the latest",{"href":305,"dataGaName":306,"dataGaLocation":40},"/the-source/","the source",{"text":308,"config":309,"lists":311},"Company",{"dataNavLevelOne":310},"company",[312],{"items":313},[314,319,325,327,332,337,342,347,352,357,362],{"text":315,"config":316},"About",{"href":317,"dataGaName":318,"dataGaLocation":40},"/company/","about",{"text":320,"config":321,"footerGa":324},"Jobs",{"href":322,"dataGaName":323,"dataGaLocation":40},"/jobs/","jobs",{"dataGaName":323},{"text":285,"config":326},{"href":287,"dataGaName":288,"dataGaLocation":40},{"text":328,"config":329},"Leadership",{"href":330,"dataGaName":331,"dataGaLocation":40},"/company/team/e-group/","leadership",{"text":333,"config":334},"Team",{"href":335,"dataGaName":336,"dataGaLocation":40},"/company/team/","team",{"text":338,"config":339},"Handbook",{"href":340,"dataGaName":341,"dataGaLocation":40},"https://handbook.gitlab.com/","handbook",{"text":343,"config":344},"Investor relations",{"href":345,"dataGaName":346,"dataGaLocation":40},"https://ir.gitlab.com/","investor relations",{"text":348,"config":349},"Trust Center",{"href":350,"dataGaName":351,"dataGaLocation":40},"/security/","trust center",{"text":353,"config":354},"AI Transparency Center",{"href":355,"dataGaName":356,"dataGaLocation":40},"/ai-transparency-center/","ai transparency center",{"text":358,"config":359},"Newsletter",{"href":360,"dataGaName":361,"dataGaLocation":40},"/company/contact/","newsletter",{"text":363,"config":364},"Press",{"href":365,"dataGaName":366,"dataGaLocation":40},"/press/","press",{"text":368,"config":369,"lists":370},"Contact us",{"dataNavLevelOne":310},[371],{"items":372},[373,376,381],{"text":47,"config":374},{"href":49,"dataGaName":375,"dataGaLocation":40},"talk to sales",{"text":377,"config":378},"Get help",{"href":379,"dataGaName":380,"dataGaLocation":40},"/support/","get help",{"text":382,"config":383},"Customer portal",{"href":384,"dataGaName":385,"dataGaLocation":40},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":387,"login":388,"suggestions":395},"Close",{"text":389,"link":390},"To search repositories and projects, login to",{"text":391,"config":392},"gitlab.com",{"href":54,"dataGaName":393,"dataGaLocation":394},"search login","search",{"text":396,"default":397},"Suggestions",[398,400,404,406,410,414],{"text":69,"config":399},{"href":74,"dataGaName":69,"dataGaLocation":394},{"text":401,"config":402},"Code Suggestions (AI)",{"href":403,"dataGaName":401,"dataGaLocation":394},"/solutions/code-suggestions/",{"text":121,"config":405},{"href":123,"dataGaName":121,"dataGaLocation":394},{"text":407,"config":408},"GitLab on AWS",{"href":409,"dataGaName":407,"dataGaLocation":394},"/partners/technology-partners/aws/",{"text":411,"config":412},"GitLab on Google Cloud",{"href":413,"dataGaName":411,"dataGaLocation":394},"/partners/technology-partners/google-cloud-platform/",{"text":415,"config":416},"Why GitLab?",{"href":82,"dataGaName":415,"dataGaLocation":394},{"freeTrial":418,"mobileIcon":423,"desktopIcon":428,"secondaryButton":431},{"text":419,"config":420},"Start free trial",{"href":421,"dataGaName":45,"dataGaLocation":422},"https://gitlab.com/-/trials/new/","nav",{"altText":424,"config":425},"Gitlab Icon",{"src":426,"dataGaName":427,"dataGaLocation":422},"/images/brand/gitlab-logo-tanuki.svg","gitlab icon",{"altText":424,"config":429},{"src":430,"dataGaName":427,"dataGaLocation":422},"/images/brand/gitlab-logo-type.svg",{"text":432,"config":433},"Get Started",{"href":434,"dataGaName":435,"dataGaLocation":422},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":437,"mobileIcon":441,"desktopIcon":443},{"text":438,"config":439},"Learn more about GitLab Duo",{"href":74,"dataGaName":440,"dataGaLocation":422},"gitlab duo",{"altText":424,"config":442},{"src":426,"dataGaName":427,"dataGaLocation":422},{"altText":424,"config":444},{"src":430,"dataGaName":427,"dataGaLocation":422},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":450,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"title":451,"button":452,"config":456,"_id":458,"_type":26,"_source":28,"_file":459,"_stem":460,"_extension":31},"/shared/en-us/banner","GitLab Duo Agent Platform is now in public beta!",{"text":80,"config":453},{"href":454,"dataGaName":455,"dataGaLocation":40},"/gitlab-duo/agent-platform/","duo banner",{"layout":457},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":462,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"data":463,"_id":668,"_type":26,"title":669,"_source":28,"_file":670,"_stem":671,"_extension":31},"/shared/en-us/main-footer",{"text":464,"source":465,"edit":471,"contribute":476,"config":481,"items":486,"minimal":660},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":466,"config":467},"View page source",{"href":468,"dataGaName":469,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":472,"config":473},"Edit this page",{"href":474,"dataGaName":475,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":477,"config":478},"Please contribute",{"href":479,"dataGaName":480,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":482,"facebook":483,"youtube":484,"linkedin":485},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[487,510,567,596,630],{"title":58,"links":488,"subMenu":493},[489],{"text":490,"config":491},"DevSecOps platform",{"href":67,"dataGaName":492,"dataGaLocation":470},"devsecops platform",[494],{"title":198,"links":495},[496,500,505],{"text":497,"config":498},"View plans",{"href":200,"dataGaName":499,"dataGaLocation":470},"view plans",{"text":501,"config":502},"Why Premium?",{"href":503,"dataGaName":504,"dataGaLocation":470},"/pricing/premium/","why premium",{"text":506,"config":507},"Why Ultimate?",{"href":508,"dataGaName":509,"dataGaLocation":470},"/pricing/ultimate/","why ultimate",{"title":511,"links":512},"Solutions",[513,518,521,523,528,533,537,540,544,549,551,554,557,562],{"text":514,"config":515},"Digital transformation",{"href":516,"dataGaName":517,"dataGaLocation":470},"/topics/digital-transformation/","digital transformation",{"text":146,"config":519},{"href":141,"dataGaName":520,"dataGaLocation":470},"security & compliance",{"text":135,"config":522},{"href":117,"dataGaName":118,"dataGaLocation":470},{"text":524,"config":525},"Agile development",{"href":526,"dataGaName":527,"dataGaLocation":470},"/solutions/agile-delivery/","agile delivery",{"text":529,"config":530},"Cloud transformation",{"href":531,"dataGaName":532,"dataGaLocation":470},"/topics/cloud-native/","cloud transformation",{"text":534,"config":535},"SCM",{"href":131,"dataGaName":536,"dataGaLocation":470},"source code management",{"text":121,"config":538},{"href":123,"dataGaName":539,"dataGaLocation":470},"continuous integration & delivery",{"text":541,"config":542},"Value stream management",{"href":173,"dataGaName":543,"dataGaLocation":470},"value stream management",{"text":545,"config":546},"GitOps",{"href":547,"dataGaName":548,"dataGaLocation":470},"/solutions/gitops/","gitops",{"text":183,"config":550},{"href":185,"dataGaName":186,"dataGaLocation":470},{"text":552,"config":553},"Small business",{"href":190,"dataGaName":191,"dataGaLocation":470},{"text":555,"config":556},"Public sector",{"href":195,"dataGaName":196,"dataGaLocation":470},{"text":558,"config":559},"Education",{"href":560,"dataGaName":561,"dataGaLocation":470},"/solutions/education/","education",{"text":563,"config":564},"Financial services",{"href":565,"dataGaName":566,"dataGaLocation":470},"/solutions/finance/","financial services",{"title":203,"links":568},[569,571,573,575,578,580,582,584,586,588,590,592,594],{"text":215,"config":570},{"href":217,"dataGaName":218,"dataGaLocation":470},{"text":220,"config":572},{"href":222,"dataGaName":223,"dataGaLocation":470},{"text":225,"config":574},{"href":227,"dataGaName":228,"dataGaLocation":470},{"text":230,"config":576},{"href":232,"dataGaName":577,"dataGaLocation":470},"docs",{"text":253,"config":579},{"href":255,"dataGaName":5,"dataGaLocation":470},{"text":248,"config":581},{"href":250,"dataGaName":251,"dataGaLocation":470},{"text":257,"config":583},{"href":259,"dataGaName":260,"dataGaLocation":470},{"text":270,"config":585},{"href":272,"dataGaName":273,"dataGaLocation":470},{"text":262,"config":587},{"href":264,"dataGaName":265,"dataGaLocation":470},{"text":275,"config":589},{"href":277,"dataGaName":278,"dataGaLocation":470},{"text":280,"config":591},{"href":282,"dataGaName":283,"dataGaLocation":470},{"text":285,"config":593},{"href":287,"dataGaName":288,"dataGaLocation":470},{"text":290,"config":595},{"href":292,"dataGaName":293,"dataGaLocation":470},{"title":308,"links":597},[598,600,602,604,606,608,610,614,619,621,623,625],{"text":315,"config":599},{"href":317,"dataGaName":310,"dataGaLocation":470},{"text":320,"config":601},{"href":322,"dataGaName":323,"dataGaLocation":470},{"text":328,"config":603},{"href":330,"dataGaName":331,"dataGaLocation":470},{"text":333,"config":605},{"href":335,"dataGaName":336,"dataGaLocation":470},{"text":338,"config":607},{"href":340,"dataGaName":341,"dataGaLocation":470},{"text":343,"config":609},{"href":345,"dataGaName":346,"dataGaLocation":470},{"text":611,"config":612},"Sustainability",{"href":613,"dataGaName":611,"dataGaLocation":470},"/sustainability/",{"text":615,"config":616},"Diversity, inclusion and belonging (DIB)",{"href":617,"dataGaName":618,"dataGaLocation":470},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":348,"config":620},{"href":350,"dataGaName":351,"dataGaLocation":470},{"text":358,"config":622},{"href":360,"dataGaName":361,"dataGaLocation":470},{"text":363,"config":624},{"href":365,"dataGaName":366,"dataGaLocation":470},{"text":626,"config":627},"Modern Slavery Transparency Statement",{"href":628,"dataGaName":629,"dataGaLocation":470},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"title":631,"links":632},"Contact Us",[633,636,638,640,645,650,655],{"text":634,"config":635},"Contact an expert",{"href":49,"dataGaName":50,"dataGaLocation":470},{"text":377,"config":637},{"href":379,"dataGaName":380,"dataGaLocation":470},{"text":382,"config":639},{"href":384,"dataGaName":385,"dataGaLocation":470},{"text":641,"config":642},"Status",{"href":643,"dataGaName":644,"dataGaLocation":470},"https://status.gitlab.com/","status",{"text":646,"config":647},"Terms of use",{"href":648,"dataGaName":649,"dataGaLocation":470},"/terms/","terms of use",{"text":651,"config":652},"Privacy statement",{"href":653,"dataGaName":654,"dataGaLocation":470},"/privacy/","privacy statement",{"text":656,"config":657},"Cookie preferences",{"dataGaName":658,"dataGaLocation":470,"id":659,"isOneTrustButton":103},"cookie preferences","ot-sdk-btn",{"items":661},[662,664,666],{"text":646,"config":663},{"href":648,"dataGaName":649,"dataGaLocation":470},{"text":651,"config":665},{"href":653,"dataGaName":654,"dataGaLocation":470},{"text":656,"config":667},{"dataGaName":658,"dataGaLocation":470,"id":659,"isOneTrustButton":103},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[673],{"_path":674,"_dir":675,"_draft":6,"_partial":6,"_locale":7,"content":676,"config":680,"_id":682,"_type":26,"title":18,"_source":28,"_file":683,"_stem":684,"_extension":31},"/en-us/blog/authors/jacob-vosmaer","authors",{"name":18,"config":677},{"headshot":678,"ctfId":679},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659488/Blog/Author%20Headshots/gitlab-logo-extra-whitespace.png","Jacob-Vosmaer",{"template":681},"BlogAuthor","content:en-us:blog:authors:jacob-vosmaer.yml","en-us/blog/authors/jacob-vosmaer.yml","en-us/blog/authors/jacob-vosmaer",{"_path":686,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"header":687,"eyebrow":688,"blurb":689,"button":690,"secondaryButton":694,"_id":696,"_type":26,"title":697,"_source":28,"_file":698,"_stem":699,"_extension":31},"/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":42,"config":691},{"href":692,"dataGaName":45,"dataGaLocation":693},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":47,"config":695},{"href":49,"dataGaName":50,"dataGaLocation":693},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",1753475342841]