Participants
- Caneco
- Pedro Oliveira (Landing.jobs)
- José Postiga (Infraspeak)
Recently I needed to tap on Laravel’s Passport programming to control how the JSON Web Tokens (JWT) were being issued. Specifically, I needed to add more claims to it (to hold more user information) and to control how the scopes were being generated. The idea was to add information like the authenticated user’s email, VAT number, account type, and, also, to forcibly add the scopes that were associated with the user’s role.
However, changing Laravel Passport’s behavior to make it happen isn’t quite obvious, and in a world were service-oriented architectures are becoming ever more common, JWT being the de facto way of carrying user’s information through multiple services, and since I couldn’t find quite a good resource to understand how to do it, I thought I could share with you how I approached and solved my own problem.
I’m developing ITsoup, an IT support/helpdesk software system, designed to simplify and streamline all its related processes, based in a service-oriented architecture. This means that I’ll have many services that handle specific domains of work. In order to authenticate the requests, properly, I needed some way of holding users’ information in a way that could be passed around from service to service. The de facto way of doing this is with JWTs, so I started investigating how I could make those changes to the JWT generation logic.
The problem with current Laravel Passport implementation is that it only includes one identification claim for the user: it’s internal ID. That’s not of much use for me on this project. Using only the ID would require any other services to request any additional user information to the Organization Domain service’s API if they need, for example, the e-mail.
Doing that way would create an unwanted, unnecessary, dependency between this service and all others that work with the user’s data and would increase the load on this Organization Domain service as new services and/or traffic increased.
Before anything, I like to always understand what’s going on under the hood. By getting a broader context of the system I need to change, I can make informed decisions on how to approach a solution.
As far as I understood, it all goes down on Laravel\Passport\PassportServiceProvider. Here we can find the registerAuthorizationServer() method, which is responsible for defining how Laravel instantiates the AuthorizationServer class and registers the supported oAuth2 authorization grants. But before that happens, there’s a call to a makeAuthorizationServer() method. This method is the one that’s responsible to define how the AuthorizationServer gets instantiated, and injects the proper dependencies from the Laravel’s Container!
The key class to this whole thing is the Bridge\AccessTokenRepository, which is one of the dependencies injected. Since it’s being injected via the Container, we can take advantage of it and inject our own instance of that class, instead. This class has a very special method, getNewToken(), which is called when an enabled authorization grant needs to generate a new token.
Hooking to the logic flow, here, and direct it through our own implementation of the AccessTokenRepository class enables us to control how the JWT is created, the information it holds, and many other aspects of it.
So, now that I’ve pinpointed exactly the class that I need to override, I defined that a call to the Bridge\AccessTokenRepository class, within Laravel’s Container, would return an instance of my own AccessTokenRepository class, which would extend the previous one but with the single detail of overriding the getNewToken() method. As of this moment, I successfully routed the logic to my own class.
Now, this method needs to return an implementation of the AccessTokenEntityInterface. This interface defines how to compute an access token (a JWT, for example). Laravel’s Passport implementation is exactly the one that The PHP League’s implements. In fact, not wanting to override that default behavior is one of the main reasons that Passport doesn’t supply any simple solution around this problem. In order for being able to add more claims, and control how JWT is generated – and even the scopes associated – we need to override this exact implementation.
At this point, I just needed to return an implementation of the AccessTokenEntityInterface that suited my needs. There’re two methods, specifically, that I needed to override: convertToJWT() and getScopes(). The first is the one that actually generates the JWT, and the second one is the one that compiles the scopes to associate with that JWT.
So, basically, my extended convertToJWT() method looked like this:
private function convertToJWT(CryptKey $privateKey): Token
{
return (new Builder())
->permittedFor($this->getClient()->getIdentifier())
->identifiedBy($this->getIdentifier())
->issuedAt(\time())
->canOnlyBeUsedAfter(\time())
->expiresAt($this->getExpiryDateTime()->getTimestamp())
->relatedTo((string) $this->getUserIdentifier())
->withClaim('scopes', $this->getScopes())
->withClaim('customer_id', $this->user->customer_id)
->withClaim('vat_number', $this->user->vat_number)
->withClaim('name', $this->user->name)
->withClaim('email', $this->user->email)
->withClaim('account_type', $this->user->account_type)
->getToken(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()));
}
And my getScopes() method looked like this:
public function getScopes(): array
{
return $this->user
->roles()
->pluck('scopes')
->flatten()
->unique()
->map(static function ($scope) {
return new Scope($scope);
})
->toArray();
}
Now, the JWT is generated with what I defined to be the relevant user information, and can be passed around and, hopefully, reduce the need to query the Organization Domain service if the JWT itself already holds the data needed.
We’ve recently launched Place Checkup, a web application that allows users to check if a given place follows the WHO recommended safety and prevention measures against COVID-19. Business owners, which are not yet Infraspeak‘s customers, can register through this website, claim their places and register how and what measures they’re actually following. In the end, they get an A, B, or C badge qualifying their places.
Since Infraspeak’s core software business is directly related to infrastructure maintenance management, and since we develop with an API-first approach, we already had the core functionalities available to easily use on Place Checkup. Besides that, and to avoid having multiple accounts spread over different projects, we defined that we’d leverage our existent API authentication, and centralize all the accounts there.
Our API uses Passport, an oAuth2 server implementation for Laravel projects, so by sending a user’s credentials to the API service, if they are valid, it returns an encoded JWT with the user information we’d need. To keep things simple, and to avoid having to implement a Machine-to-Machine authentication, we wanted to simply store the generated JWT in session, on Place Checkup, for further usage on subsequent calls to the API.
Laravel has a very good, and extendable, authentication system, referenced throughout the documentation as Guards. Their job is to know how an authentication request gets processed and how to communicate with a Provider, whose responsibility is to know how to get the requested data from whatever persistent layer it handles.
If correctly implemented, a custom Guard would allow us to tap into middlewares, facades, and helper methods commonly used within Laravel to handle authentication, like the Auth facade, or getting the currently authenticated user via $request->user(), or even use the auth middleware to secure access to private routes. That looked exactly what we needed for this project!
But adding a custom Guard and Provider proved to be a little more difficult than what the documentation shows to be. Reading through the sections that describe how Guards and Providers work, I could only see how to register the custom classes to the Container, which interfaces to implement, and which type of classes it needs to return. I couldn’t quite get a clear picture of how the flow was, so I realized I’d need to dive deep into the authentication system and try to map it from within.
Laravel’s documentation describes Guards as the following:
Guards define how users are authenticated for each request.
But, what does that really mean? That was the question I had on my head while deep on the SessionGuard class – a preexistent Guard shipped by default with the framework – trying to understand what’s going on under the hood. I figured that a Guard has the sole responsibility of receiving requests to authenticate a user and to call the configured Provider to fetch that user on the persistent layer.
This means that, for example, when we call Auth::attempt() to attempt a login action, Laravel will defer that call to the same method on the configured Guard. This Guard will, in turn, prepare all it needs to send that request through the related Provider, which may (or may not) return an instance of the Authenticatable interface, the object that represents the authenticated user.
I’ve also found that we actually have two interfaces for Guards:
Guard interface, referenced in the docs, aimed to a stateless authentication, like APIs;StatefulGuard interface, that extends the previous Guard interface but also defines how to persist the authentication information in the configured session storage.The fundamental difference between them: state management. The knowledge of the existence of this StatefulGuardinterface pointed me to a good direction of how I could develop a Guard that would persist the JWT in the session that integrates perfectly with the expectations of Laravel itself. I was finally in a good place on understanding the key concepts of the whole authentication system.
Laravel’s documentation describes Providers as the following:
Providers define how users are retrieved from your persistent storage.
Looking at the two default providers that Laravel supports, EloquentUserProvider and DatabaseUserProvider, I noticed that first returns an instance of Model (an Eloquent model class) and the other handles direct calls to a database table and returns an instance of GenericUser (a slight variation of a Value Object). Both follow the same UserProviderinterface.
I concluded that Providers are an implementation of the Repository pattern, and exist only to abstract away how you validate the user’s credentials and fetch their data on whatever persistent layer it’s stored in.
Actually, that’s probably why the user’s credentials are passed on inside an array structure: because Laravel doesn’t want to be opinionated towards how you authenticate your users. You can use a combination of e-mail and password, or a one-time hash (like magic links sent to an e-mail), or whatever you want to use. It’s pretty clever, actually!
But it’s basically this… It’s inside a Provider that you actually define what data you need to authenticate a user (the credentials) and how you communicate with the persistent layer to get the relevant data. No matter how that’s done, a Provider must return an object that implements the Authenticatable interface. This is to ensure that Laravel uses the correct methods when working with that object, directly, like when calling Auth::id().
I’m pretty sure that at this point you already have a clearer understanding of the role Guards and Providers take within the Laravel’s authentication workflow: Guards receive the request to authenticate a user and pass the credentials to the defined Provider. The Provider, in turn, queries the underlying persistent storage about the relevant user data associated with those credentials and returns it all the way back to the Guard, which makes that information available for usage.
But even if you create all the required classes, you still need to register them within Laravel’s Container, so that it knows how to call those classes:
Auth::provider() method and define how the framework should instantiate the custom provider class;Auth::extend() method and define how the framework should instantiate the custom Guard.Let me show you exactly what we’ve done to register the custom Guard and Provider used on the Place Checkup project:
public function boot(): void
{
Auth::provider('infraspeak-users', static function (Application $app, array $config) {
return new InfraspeakUserProvider($app->make(InfraspeakApiService::class), $config);
});
Auth::extend('infraspeak-jwt', static function (Application $app, $name, array $config) {
return new JwtSessionGuard(
Auth::createUserProvider($config['provider']),
$app->make('session.store')
);
});
}
The InfraspeakUserProvider class has all the code to communicate with our API service and validate a user’s credentials and returns an instance of the Authenticatable interface. The JwtSessionGuard class has the code that takes that object, from the Provider, and persists it to the session, avoiding new calls being dispatched to the API service while that session is active and valid, protecting our API service from being flooded with authentication requests every time a protected endpoint is being processed.
The very final step in all this work was to update the config/auth.php, and switch the default Guard and Provider used for the web routes:
'guards' => [
'web' => [
'driver' => 'infraspeak-jwt',
'provider' => 'infraspeak-users',
],
],
Now we could use Laravel’s native classes to authenticate a user and use our API service to serve as the data repository to get that information.
Wow! That’s the perfect word to describe my 2019. So far, my best year ever!
My 2019 started with me leaving TBFiles and joining Infraspeak. The change came as a surprise, even to me. I was not looking for a job change, but a casual visit to the Infraspeak’s HQ, to discuss the development of a Laravel related event, which led me to meet a lot of my (soon-to-be) colleagues and the company’s vision, goals, and culture, impressed me so much that I got stuck with the feeling that I belonged there.
My work, at Infraspeak, ranged from developing new, and exciting, features, to help to improve the overall code quality, improving the development workflow and stack used and implementing a Continuous Integration pipeline. I’ve been learning and growing so much! It’s definitely the best place to work, right now. Consider applying to one of our job openings.
There’s a lot more I plan to do here, in 2020, which I’ll blog about on Infraspeak’s Tech Blog.
I did less open-source work than what I intended. In fact, I didn’t even complete 2019’s Hacktoberfest, with only two PRs made…
However, I contributed to the akaunting/akaunting and spatie/laravel-permission projects, created a side project that I’m still actively developing (SpeakHub) and I’ve joined the VOST Portugal organization, where I’ve been helping in the API layer development and maintenance.
For 2020, I’ll be releasing a beta version of my side project and dedicate more time to Open Source contributions. I’ll definitely not miss this year’s Hacktoberfest challenge!
I tried something new in this area: I organized a Meetup around the Portuguese Laravel community. I tried to organize another one, but several things happened that made it impossible to create a second event. In 2020, however, there will be a new edition of this event.
I’ve also attended some events, too. I was present in the first Laracon Madrid, representing the Infraspeak Engineering Team. I met a lot of new people there, got to personally thank some of my favorite creators, like Christoph Rumpel, Freek Van der Herten and Adam Wathan. I got to be with other Laravel Portugal members and even fellow podcasters Nuno Maduro, Caneco and Bruno Falcão. I’ve also attended TechInPorto, a tech-related event in the beautiful Porto city.
For 2020, I’ll aim to speak on one conference, I’ll try to attend not only TechInPorto 2020 but also Laracon Madrid 2020 and I’ll definitely organise the second edition of Laravel Portugal meetup.
I’ve published three articles and twenty-one journal entries. It was amazing getting to write and share this much information with the developers’ community.
I’ve also been invited to participate in the amazing Ubuntu Portugal Podcast, where I discussed my transition from the Apple ecosystem to Ubuntu. I had a very good and friendly talk with the hosts Diogo Constantino e David Negreira.
Unfortunately, the Laravel Portugal Podcast only had one episode recorded in the whole year. It’s so sad that we (me and the other hosts) didn’t get enough time to do it more often. I always believed that a Portuguese podcast about the Laravel ecosystem makes sense to exist.
For 2020, I’ll definitely try to resurrect the Laravel Portugal Podcast, making it a once or twice per month event. Also, I’ll also make a weekly journal entry to round-up about work done and other relevant events. Maybe I’ll make a 2-in-1 and we might have a weekly Portuguese podcast about that. Who knows…
This last year was a year full of experiments and trials and errors. As I close 2019 as the best year ever, so far, I look for 2020 with a lot of expectations to surpass the previous one. I’ll definitely begin to see the results of my work in Infraspeak, and will channel all my experience gathered until now towards open source and community development.
Wishing you all the best for this new year!
Yeah, it sucks.
Last week I tried to contribute to Mohammed Said’s Wink Laravel package (https://github.com/writingink/wink) in order to make it use the Laravel’s default auth system, since Wik uses its own guards, register and login workflow.
After a couple days of work, and a nice discussion on the PR, Mohammed though that it was best not to merge the PR because he wanted it to be a more flexible solution as my PR would force Wink to use Laravel’s default database connection and would update the users table with Wink specific columns.
Although the feeling of rejection sucks, I thank Mohammed, and others that commented on my PR, as they allowed me to grow and learn that contributing with open source is not all about quantity of PRs merged, but the quality of the products afterwards.
But this isn’t over, yet. We’re still discussing a better approach to this situation. If you’re interesting what we’re planning, check the issue on GitHub: https://github.com/writingink/wink/issues/13.
This article was originally submitted on the Infraspeak tech blog.
The hosts file is used by the system’s DNS resolver to map a fully qualified domain name (FQDN) to its related IP, without the need to query any of the Internet’s DNS servers. However, since updating this file, manually, by the common computer user would be impractical, its common use is to map the local IP 127.0.0.1 to localhost domain so that it resolves to the host machine.
So, since this file overrides the default behavior of querying the Internet’s DNS servers, and directly maps any string of characters to an IP, it’s frequently used by programmers to associate a development domain with the localhost IP address. This enables the use of a non-existent domain like, for example, my-secret-project.test as an FQDN on a browser, as you’d normally access a registered .com (or any other TLD) domain.
At Infraspeak, we have several projects that use personalized domain names in our development environment. This requires that each of those domain names have to be mapped in the hosts file of every developer’s computer. Every time we use a new, or change an existent, project domain name, we have to warn everybody about it, and each developer has to update the hosts file.
Since we’re investing a lot in hiring, it got me thinking about the tedious work of every new colleague editing the same file and paste the same mappings over and over again, and I decided to investigate a way of automating the process. I’ve come across several infrastructure-related projects, in my past, that does this. Valet, for example, which is a Laravel development environment, does this exact thing: it routes all requests for domains that use the .test TLD to a preconfigured folder with the same name of the domain requested. So, a request to my-secret-project.test would route to a project inside a my-secret-project folder.
I knew that Valet leveraged Dnsmasq to do that, so I went to read more about how that software works and how could I add it to my stack and route the development domains, without editing the hosts file. While following the documentation to install it in my machine, I got an error related to port 53 being already in use. While trying to find which service was using that port, I found that a package named dnsmasq-base was already installed on my Ubuntu machine.
I don’t really know what Ubuntu is using that package for, or if it was really used at all, but I guess that it could be that the NetworkManager was leveraging the capabilities of DNS caching that Dnsmasq has. Anyway, the fact that it was already installed saved me some steps on configuring the environment. Now, I just needed to find a way to configure the Ubuntu NetworkManager to use Dnsmasq instead of system-resolve.
A little more time investigating showed me that NetworkManager already supports Dnsmasq out-of-the-box and that to enable its use all that’s required is a single key in the [main] section of its configuration file. That’s simple enough, but I’ve been working with Linux for a lot of time now, to know that almost every system’s service has a conf.d folder that’s used to override the default configuration with the use of partial overriding of variables. Without any surprise, I found that the NetworkManager was no exception to this.
That meant I simply needed to add a file to the conf.d folder with only the configuration needed to activate the Dnsmasq support. I called the file dnsmasq.conf and put it inside the folder /etc/NetworkManager/conf.d, with the following content:
[main]
dns=dnsmasq
It was that simple! Now, the NetworkManager service was being instructed to use Dnsmasq to resolve all domains, instead of the native system-resolve service. However, this configuration alone was insufficient to route all local development domains to the localhost IP address. I still needed to add a specific Dnsmasq configuration file, to instruct it to not query the Internet’s DNS servers for requests of specific TLDs (our development TLDs).
The file that was missing needed to be inside the dnsmasq.d folder, which is very close to the previous folder: /etc/NetworkManager/dnsmasq.d. Creating a file there, with the name development-tld.conf, and adding a single line with address=/development/127.0.0.1 is all that was missing. Now, Dnsmasq won’t query the Internet’s DNS servers to know what’s the IP for every domain that uses .development TLD.
We use
.developmentas a local development TLD, but we could use the exact same steps to use any other TLD, like.testJust take note that whatever you use, won’t reach the Internet. So, if you use.com, for example, all requests for domains with that TLD will try to be resolved to your machine. Stick with a TLD that you know it’s not a “valid” one on the Internet, to avoid such problems.
Finally, I had everything properly configured. I only needed to disable the system-resolve service, permanently, and restart the NetworkManager service. The commands to do so were the following:
sudo systemctl disable systemd-resolved.service
sudo systemctl stop systemd-resolved.service
sudo rm /etc/resolv.conf
sudo systemctl restart network-manager.service
The line sudo rm /etc/resolv.conf forces the O.S. to regenerate the file with the new, updated, configuration, and the last line forces NetworkManager to load the all-new configuration.
Now I had all my development domains auto-resolving to the localhost machine, even with domains that I never configured in the hosts file. Here’s an excerpt of what happens when pinging any domain with the configured .development TLD:
▶ ping infraspeak.development
PING infraspeak.development (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.038 ms
--- infraspeak.development ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 32ms
rtt min/avg/max/mdev = 0.025/0.031/0.038/0.008 ms
▶ ping non-existing-subdomain.infraspeak.development
PING non-existing-subdomain.infraspeak.development (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.041 ms
--- non-existing-subdomain.infraspeak.development ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 20ms
rtt min/avg/max/mdev = 0.027/0.034/0.041/0.007 ms
▶ ping customer-x.infraspeak.development
PING customer-x.infraspeak.development (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.045 ms
--- customer-x.infraspeak.development ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 23ms
rtt min/avg/max/mdev = 0.025/0.035/0.045/0.010 ms
Pretty sweet, right? No more messing around the hosts file!
This article was originally submitted on the Infraspeak tech blog.
Infraspeak is a startup founded in 2015, focused on developing the best maintenance management software in the market. Built around simplicity and a user-friendly UI/UX, we have more than 25000 buildings being managed every day, with the help of our product.
You might think that due to this number, there’s a big team of software engineers developing and maintaining the software. You’d be wrong. The Product Team is composed of 12 people, distributed between Backend, Frontend, Mobile, AI, and Integrations, and I’m also counting with the CTO.
Although we’re actively recruiting new developers to the team, we’re perfectly comfortable in maintaining such a user base and still making sure we keep improving the codebase with optimizations, as well as deploying new features because we automate as much as possible. One automation we have in place is the Continuous Integration pipeline.
The need for a Continuous Integration pipeline came from the fact that we were quickly becoming a critical dependency for our customers daily operations and, because of that, we needed to focus on constant quality assurance of our work. We needed a way of automating the integration testing of the work that was merged to the master branch (which is then deployed to production) with as minimal human interference as possible.
When searching for options for a pipeline system, we were looking for something simple and quick to implement, but also easy to maintain. We had three ways of doing this:
Since we have a relatively small team, allocating resources to configure a dedicated CI infrastructure, and actively support it, was very hard to reason about, so this option was quickly disregarded. We looked at Travis CI and Circle CI with good eyes, because we didn’t have to handle the maintenance ourselves, but even being a well-funded company, their pricing plans were very expensive for our first attempt at using a pipeline system. We needed to gather more information, and experience, before requesting more resources to be invested in this.
In the end, we went with Bitbucket Pipelines. Since we were already a paid customer, we had access to a 500 minutes pipeline execution plan without any additional costs. For what we wanted to do at the moment, it was good enough.
Activating Bitbucket Pipelines was as simple as clicking on the dedicated “Enable Pipelines” button, available in the repository settings page. The interesting and a little bit more complicated part was creating the configuration YAML file according to the project specification so that Bitbucket knew when to run and how to instantiate and automatically configure the pipeline’s infrastructure. The main goal was to activate the pipeline in specific points on our branching strategy.
We have a pretty standard branching strategy: we have a master branch, always in sync with production, we have a development branch and we have n working branches corresponding to an active task. As soon as developers are finished with their task, they send a PR targeting the development branch, which then is peer-reviewed and, finally, merged. At the end of the sprint cycle, the development branch gets merged to master and deployed to production.
To be confident about the changes we continuously made to the project, and to minimize the need for manual testing between merges to the development and the master branch, without sacrificing speed and agility of development, we needed the pipeline to run on key events of our workflow:
Because we had a limited execution time, we couldn’t include every push to the working branches but assumed that each developer would run the full test suite locally and only push the changes when they had them all passing. The pipeline would only continuously check the integration of all developers code into the development and the master branch, as we knew that those were the points in time where bugs, conflicts, and other problems could happen.
We decided that the first project which we were to have the pipeline configured to test was our main API layer. It’s where we have the main business logic and data, making it one of the main critical points of failure to our business. If it failed, then all other tools we provide were going to fail too. Having this in mind, we started to map all the steps required to automatically instantiate the project and run the full test suite (unit and feature tests):
Digging through the Bitbucket Pipelines documentation, we learned that it’s all run inside a dockerized environment and that the main base image they recommend using would check out the correct branch and make the code available to all other steps of the pipeline. That was exactly what we needed to handle the first step.
For step two, we needed to have a way to run Composer, to install the dependencies, and run PHP for the test suite. The Bitbucket Pipelines documentation states that each pipeline step can have a dedicated docker image running. That would support our initial idea for running Composer and the PHPUnit test suite in two different steps.
The documentation also refers to the fact that we can have shared containers (they call it services) running and accessible from all steps throughout the whole pipeline execution. Since we rely on PostgreSQL as our persistent layer, and we had a lot of feature tests that would use that layer, it was a very much appreciated functionality that we would definitely need.
After having the first draft of the pipeline configuration file, we created a test branch, pushed the file to the repository and watched the pipeline come to life. It started to run and we were watching happily, for about a minute, then it failed hard. The logs stated that the vendor folder, which is where Composer downloads and installs the dependencies, was nowhere to be found. It seemed that it was being removed at the end of the step (at the teardown phase).
We got to scratch our heads, a lot, about this. We were installing the dependencies in the previous step and we were declaring the composer cache strategy, which is natively supported by Bitbucket and is specifically built for keeping Composer dependencies for the next step. After a little investigation, and reading a lot of documentation, we found out that this was happening because the artifacts configuration key was missing. This configuration maps the Composer vendor folder, and all files it contains, to transition to the next step.
After updating the configuration file, we got it running successfully. The vendor folder was transitioning correctly to the next step and the test suite was passing. Everything was green and we had our first successful pipeline execution!
Since it was running, it was time to optimize it. The first optimization was around the Composer dependencies install. It was slow, taking around three minutes to install all the dependencies. Was there something we could do to improve this? After a little more digging, we found out that we should use a caching strategy. Composer, when running locally in your machine, saves a reference to the remote dependencies repositories, allowing it to skip several steps when fetching those dependencies.
Since the Composer binary, on our pipeline, was running in a docker container, the references cache were compiled but they would never persist. Docker containers don’t persist data after being destroyed, unless you add volumes to them. And that’s what we needed to add: a cache volume to persist those references.
Doing that was not an easy task. The Bitbucket Pipelines documentation states that it has a predefined caching strategy for Composer, which is awesome and would save a lot of time by not requiring us to configure a personalized one, but it forgot to mention that if you use the default Composer docker image you need to also declare the /tmp folder as the one that needs to be cached.
But after we finished updating the configuration file, the time it took to install the dependencies went down to around fifteen seconds. That’s fast! Considering that this was intended to run several times, per developer, in a normal workday, it would stretch out our available execution time cap.
And it was pretty much it. We only needed to make it run on the predefined events (pull requests and pushes to master or development branch) and we had our pipeline fully working so we made the PR to the development branch. Our final configuration file was similar to the following:
image: atlassian/default-image:latest
pipelines:
pull-requests:
'**':
- step:
name: Install dependencies
image: composer
caches:
- composer
artifacts:
- vendor/**
script:
- composer install --ignore-platform-reqs
- step:
name: Run tests
image: php:7.2-cli
script:
- ln -f -s .env.pipeline .env
- php artisan key:generate
- php artisan migrate
- php artisan passport:keys
- printf "\n" | php artisan passport:client --password
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Unit --no-coverage
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Feature --no-coverage
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Domain --no-coverage
services:
- postgres
branches:
'{master,dev-sprint-*}':
- step:
name: Install dependencies
image: composer
caches:
- composer
artifacts:
- vendor/**
script:
- composer install --ignore-platform-reqs
- step:
name: Run tests
image: php:7.2-cli
script:
- ln -f -s .env.pipeline .env
- php artisan key:generate
- php artisan migrate
- php artisan passport:keys
- printf "\n" | php artisan passport:client --password
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Unit --no-coverage
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Feature --no-coverage
- vendor/bin/phpunit -c tests/phpunit.xml --testsuite Domain --no-coverage
services:
- postgres
definitions:
caches:
composer: /tmp
services:
postgres:
image: postgres:10.5
environment:
POSTGRES_DB: database
POSTGRES_USER: root
POSTGRES_PASSWORD: root
With Bitbucket Pipelines handling our continuous integration testing workflow, every time some sneaky bug tried to enter our codebase, the pipeline would fail, informing the author of the commit via e-mail, warning to check the pipeline logs and apply the necessary correction to the code submitted.
As long as we continued to add tests for every new feature, bug fix, and improvement, the pipeline would take care of checking every test case for possible problems. This had a very positive impact in our team workflow and improved our code quality because lesser bugs were merged into the codebase. It helped the team shift a little bit more from a reactive to a more preventive position. It’s better for the pipeline to catch the bugs and break, than our customers’ catch the bugs, lose work and then lose those clients!
Docker is the best way to build, share and run applications in the cloud. There’re no doubts about that! You literally only have to configure your infrastructure once, programmatically, and can run on every cloud provider. It’s amazingly fast, too. No wonder that everyone is crazy about this technology and are using it to support their most critical business services.
Surely you’ve also heard, and read, about teams using Docker in their development process, right? That you can get up and running, on any project, without messing around with your computer and not being worried about installing different (or specific) versions of a software language or any other dependency you might need. You simply docker-compose upand all services/dependencies are spawn and ready to use. Everything gets to run on an isolated container.
I’ve been using Docker for development for a little more than two years, at the time of writing this article, and I’d like to share with you some details that’ll make your experience with Docker, for development, a very smooth one. But before I dump all the knowledge to you, we’ll start slow, by breaking my path to the final implementation into small parts, so you can understand where my decisions came from.
Let’s get to it, shall we?
Note: All examples will be oriented with web development in mind, but I think you can extrapolate to your situation, specifically.
This is not an introduction to Docker or Dockerfiles. You have the official documentation that has in-depth information about it. However, I’ll tell you this: Dockerfiles contains additional commands (steps) that are called to assemble a Docker image with configurations, dependencies and other things you might need to run your container. However, you should avoid using them for as long as you possibly can.
It’s not that these files are complicated because they’re not, it’s just that, as soon as you create a Dockerfile, you’re taking responsibility about managing that specific image definition. It’s not a decision you should take lightly. Dockerfiles, like any other system of its kind, need to be revisited from time to time and updated because, for example, a library you added has a new version available.
To avoid having this responsibility, you should first check for an official image on Docker Hub! Let others have to worry about dependency management and update. You’ll want to use your time to generate value, not worrying about dependencies versions being outdated, or even worrying about vulnerabilities on those dependencies. Docker Hub has a lot of images for you to work with. Not only has many images created by community members, but also has official images, supported by the very companies and groups that develop the underlying dependencies. For example, you can find official images for NGINX, PHP, Composer, Yarn, NodeJS, and a lot more!
However, for those cases when you really have to create one, try to use it as little as possible. The next sections will show you how.
A Dockerfile requires that you define a base image, which to apply your changes upon. This can be an O.S. version (Ubuntu 19.10, Debian 9, Fedora, Mint, etc.) or you can even use an image that already has a dependency installed (PHP 7.3, NGINX, MySQL, etc.) which already have an underlying O.S. definition and all configurations needed to run that dependency.
For example, when creating a PHP Dockerfile, I’ll try to use the most recent, stable, version needed for my situation. So, supposing I need to use PHP 7.2 (CLI), I’ll start my Dockerfile with:
FROM php:7.2-cli
Every command I add next will be run on top of this PHP 7.2 image, and have all dependencies needed to have it running without problems.
By using a base image that already has almost everything I need, I’m also limiting my responsibility. I don’t have to worry about every command it takes to install PHP and I don’t have to worry about managing or updating it. If by any chance, there’s a new version of that base image, all I have to do is run the docker build command to fetch all updates available, directly related to the base image.
To execute a command, after defining the base image, you use the RUN directive. This runs the given command during the image build process, while the final image is being compiled, and corresponds to a layer. Each layer, upon being executed, is cached. This prevents the docker build process to run every command, every time you execute it, saving you (sometimes a lot of) computation time.
So, if every RUN directive can correspond to a cached layer, it should make sense to use as few as possible, speeding up the compiling time. Well, not so fast! You need to use it with care, because every time you change the statement, the build process will re-execute the entire modified RUN directive, even if the only thing you changed was the order of the arguments. So, if you have a “big” RUN command, and change it, the cache is invalidated and you’ll have to wait that it finishes executing all work defined on that modified RUN directive and every one after.
To avoid having this problem, often, you have to find a sweet spot between the number of RUN directives and the amount of work done on each. I tend to separate mines in three sections: package installs, package configurations and packages activations.
The whole point of using Docker is to keep the dependencies at a minimum, and decoupled from your host computer. However, this line of thinking is not only applicable to the containers. You should start applying, at least the “keep dependencies at a minimum” part, with your Dockerfiles. Images are a very important part of your Docker setup: without images, you can’t have containers! They are two parts of a whole: the bigger your image’s size, the bigger the container size.
Remember that the only thing that your host and your containers share, is the kernel. Everything that you install or copy onto your image will add up to its final size. Every RUN directive that has a package install command (apt-get installor apk install) will be saved on the image. If you’re not careful, your image can become very heavy, very quickly. You might not even install that many things and still see your images reach one gigabyte of size, or even more.
One of the main enablers of that situation is the package manager’s cache system. You see, when you have to install software, a general step required is to run apt-get update (supposing you’re running Ubuntu as a base image) before running the command to install the software you need. This is because that command is responsible to download all package’s information list, available on every repository registered on the O.S. That list contains the repositories where the software is so that the package manager can download it and install it. The “real” problem is that after downloading that information, it’s cached.
That behavior is very welcome if you’re running that on your host machine, avoiding having to download that information every time you try to update your system, but in the context of a Docker image, that’s useless! After you install all the software you need, that cache is only occupying precious space as it’s not needed to run the, already installed, software. By removing that cache information, you can save a lot of space.
Again, this is not an introduction to Docker networks. You have the official documentation for in-depth information about it. Networking can be tricky, but it’s essential that you have a general understanding of how containers do networking.
When you spawn a container via docker run you can define a network by adding --network={your-network-name-here} (or the shortcut -n) and this will configure access, to this new container, to that network. This means that this container will have access to all other containers that also may be connected to the same network. But what about if you spawn a container without defining an explicit network? Well, one might think that this container won’t have access to anything, that’ll run in complete isolation. But that’s not always the case.
All containers that are spawn without a network definition are connected, automatically, to the default bridge network. So, in fact, if you start two containers without defining a network, those containers will be able to communicate with each other through that default bridge network. However, contrary to user-generated networks, which supports service discovery (calling other containers by their defined name), those two containers can only communicate only via their IP address.
This type of nuance can be very handy and it’s even more interesting if you think about using it for connecting to database instances! As long as your exposed ports don’t collide, you can then use the bounded, default, localhost port (0.0.0.0 or 127.0.0.1), specifying the exposed, mapped to host, port and connect to your database via a DBMS (pgAdmin, DBeaver, MySQL Workbench or any other of your liking). Pretty neat, I think! You can spin up two containers, one with your database instance and another with your app, and get them working together without even thinking about creating a user-defined network, without any docker-compose files and any other type of orchestration method being needed.
But when you do use a docker-compose file, there are a lot more interesting things you can do. You can define many networks, their drivers, force IP addresses, group different services into different networks and even define aliases for the same instances on different networks. So, an NGINX container can be called load-balancer on a network, and be called nginx-lb on another. It’s the exact same instance, but with different names on each of those networks.
There’s something really inconvenient in running commands through containers: any file created gets associated, by default, with the user and group 0 (the root user/group mapping). This means that you can’t edit or delete them, on your host computer, without using sudo or running a chown command to remap the file’s permissions to your own user. This might not sound very important but since I develop a lot with PHP, using composer can get messy as the vendor directory gets totally owned by root.
You might think it’s fine because, after all, composer is a package manager and external, third-party, packages are not to be edited locally. Ok, that’s fair, but imagine you use a script that generates PHP classes. Let’s say, like me, you work with Laravel, which has the artisan command line tool, that helps me generate not only classes but a lot more resource files to speed up your work. As soon as I run php artisan make:controller SomeController I won’t be able to edit it without, first, do a sudo chown {your username}: SomeController to give write access to that file. Pretty inconvenient, right? Luckily this can be avoided pretty easily!
Docker supports the mapping of personalized users/groups to which the container will run as! We can use it as a command parameter when running docker run. The parameter’s name signature is --user {user}:{group} and, by adding it, we’re explicitly saying that the command is being run by that user, under that group. With this, if we map to our own user/group values, it’ll associate the files to our account, enabling us to fully work with them as if they were generated directly on our host machine.
However, there’s a catch. Or two, in this case: 1. It only takes numbers for both user and group so you have to know, in advance, what’s yours; 2. It has no validation, whatsoever, for those numbers so, technically, you can use any number combination for user:group and still get the container to work. However, this has the same result as running as root, since you wouldn’t be able to edit without elevated access…
Don’t worry! There’s an easy way of using this properly without ever think about it. If you inspect the id command, available on your machine, you’ll notice you have two options to get the ID and GROUP of the currently, logged in, user account. You can use that to programmatically get the correct values.
Since bash commands can take sub-commands and evaluates them first, you can use id -u to get the user and id -g to get the group, before the main docker run command gets executed. So, you can run any docker container with your user:group mapping with the following partial: docker run --user $(id -u):$(id -g) {the rest of your command here}.
Now, you can run composer, artisan or any other command and have all the generated files correctly associated with your user/group. You don’t ever have to chown your way through those files, again, to be able to edit them on your IDE or code editor of choice.
Did you think I’ve forgotten about why you’re reading this article? Of course not! Let’s see how we can get a smooth docker stack installed and configured on our computer.
Like every new (tech) project, specifically infrastructure-related projects, we need to think, first, about how should we structure things. When I started to use Docker, I thought that I’d use a simple docker-compose file on the root of my project and be done with it. The infrastructure is project bounded, it’s related and tightly coupled to it, so it made sense to manage the infrastructure that way. It seemed simple, too.
However, I was not thinking about what to do with the Dockerfiles, with service-related configuration files (e.g.: NGINX conf files or PHP’s modules’ ini files). To handle this noise, I started using a .docker folder with everything inside, except the docker-compose file, which remained on the project root. This sounded liked a nice way to gently organize all my docker related files away from my project’s files, but it could still be versioned with the latter. Although there were many more files to manage, I would only have to worry about it once, so it still was “simple enough”.
Everything was fine until I started deploying things. All my infrastructure related files were deployed attached to my code. Some sort of inception was going on, where I was creating a stack that’ll deploy an application that had all infrastructure related files inside, too. I thought that, although having the infrastructure definition on the project repository looked like I was helping others deploying and testing my work faster, the truth is that I was assuming that they’d use Docker, too.
They could be using other development environments, like virtual machines or even have all the dependencies installed on the host machine, but they would still have to download all my infrastructure files. It didn’t felt clean, so I decided to separate my infrastructure files from my project’s, completely, and started to think about how could I manage it in a simple, eloquent, way. I needed to find a solution that would be easy to maintain, to version and to be simple enough to be rebuilt on another computer, if necessary.
I found myself creating a dedicated folder for this on my system account’s home directory (in Linux is /home/{username}) named Infrastructure. Inside this folder I have:
Stacks containing dedicated docker-compose files, each corresponding to a project;Volumes containing all data that I need to persist from my running containers (like the database container’s data);Scripts containing utility scripts for running containers’ commands through the console (for example, running Composer or PHP).Here’s a tree description of the folder:
├── Nginx
│ ├── certificates
│ ├── conf
│ ├── docker-compose.yml
│ └── nginx.conf
├── PHP
│ └── 7.2
│ ├── cli
│ │ ├── conf
│ │ │ └── xdebug.ini
│ │ └── Dockerfile
│ └── fpm
│ ├── conf
│ │ └── xdebug.ini
│ └── Dockerfile
├── Scripts
│ ├── composer
│ ├── dep
│ ├── mkdocs
│ ├── mysql
│ ├── php
│ ├── php5.6
│ ├── phpcs
│ ├── php-cs-fixer
│ ├── phpinsights
│ ├── psql
│ ├── redis-cli
│ └── yarn
├── Stacks
├── Volumes
└── install.sh
This structure allows me to version the whole folder, except the Volumes folder (database data should not be versioned) so I can quickly check it out on another computer, run the install.sh script and have the exact same structure and scripts available in a very short time. That file has five execution steps:
/usr/local/bin folder, allowing me to use them globally on my computer;Another detail on my infrastructure configuration is that I only use one NGINX container to serve all my (web) projects. If you’ve read any tutorial, on the web, about using Docker for development you’ll remember the service definition for a HTTP server (either NGINX, Apache or Caddy) declared on your project’s docker-compose file. There’s no need for that, really.
The reality is that if you follow those tutorials recommendations, you’ll be repeating yourself over and over, again, by copying and pasting the same server configuration, on every project you have. Unless you have a very specific need for that, you’ll most likely use the same HTTP server, with the same configuration for every project you work on.
Here’s a better way: use one, persistent, container. What do I mean with a “persistent” is a container that has the restartdirective property to unless-stopped. This marks the container to be permanently up, even if some error occurs or the computer gets powered down. The Docker daemon will always try to reboot the container as fast as possible after it stops unintentionally.
Take, for example, the following NGINX docker-compose definition:
version: "3.7"
services:
nginx-lb:
container_name: nginx
image: nginx
ports:
- 80:80
- 443:443
- 8080:8080
- 8082:8082
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certificates:/etc/nginx/certificates:ro
- ./conf:/etc/nginx/conf.d:ro
- ~/Code:/var/www/html
restart: unless-stopped
networks:
web:
networks:
web:
external: true
With this configuration, all I need to do to serve a new project is a new configuration file in the conf folder (which is mapped on the volumes section) and restart the container, besides having the source code on the Code folder. After this, the NGINX will have all subsequent requests to my new project routed to the proper project container, to be handled.
If you noted the volume mapping of my entire Code folder, is because of, unfortunately, a limitation in the NGINX itself, that requires the reading the file, first, to interpret the configuration rules and calculate which server/location blocks to route the request to.
Having our reverse-proxy setup is nice, but we’re still missing the project container instance, so that the requests are processed. Here’s an example docker-compose file for spinning a project’s container:
version: "3.7"
services:
awesome-project:
build: ../../PHP/7.2/fpm
image: josepostiga/php:7.2-fpm
user: "1000:1000"
expose:
- 9000
volumes:
- ~/Code:/var/www/html
restart: unless-stopped
networks:
- web
- app
networks:
app:
web:
external: true
Contrary to what you may find on various online articles, there’s no HTTP server service definition. Like I said, there’s no need, since we already have it prepared to handle many projects.
But let’s break this file, shall we? This file defines that we’ll have a PHP 7.2 container, that’ll be executing under a mapped user within system’s ID/GROUP 1000 (remember these section, from before?), that exposes port 9000 to the host. Also, it has the host’s Code folder mapped to the container’s /var/www/html folder (that’s where your code will exist), will always restart unless we issue a command to stop it (voluntarily) and has access to two networks: the web and app.
Take a moment to sink this information in…
Have you wondered why it has two networks defined? Allow me to explain: it’s to separate and isolate access to services that only concerns this stack. For example, the database container should not be on the web network, because there’s another container, the NGINX one, that also has access to that network. Does it makes sense to have them both on the same layer? No. We’re simply talking about development environment, here, but it’s not difficult to imagine a similar stack deployed on production. Having unrelated services accessing the same network is a bad habit, because anyone with access to your NGINX container can, too, have access to your database container.
It’s a good habit to think about proper boundaries on your services and limit access where it’s not needed. So, the webnetwork allows the NGINX container communicate with this PHP container and the app network allows this PHP container to access other, more reserved, services (like a database). You can add as many networks as you may see fit. Also, since the app network is not marked as external, Docker will have it namespaced to this stack, only, and even if you have the same name on any other project, they will not be able to access each other’s services. It’s a neat security feature, built-in with Docker itself.
Ok, now that we have both the reverse proxy and a project’s container to serve, we need to understand how that all fits together and enable communications between the two services.
When a request comes to NGINX, it’ll scan it to determine which domain it needs to route the request to the correct container. This happens on, what NGINX calls it, the server blocks. That’s why I’ve mapped a conf folder on the NGINX container: that’s the folder that will contain all the dedicated projects configuration files. Here’s an example of such file:
server {
server_name awesome-project.test;
listen 80;
root /var/www/html/awesome-project/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }
error_page 404 /index.php;
location ~ \.php$ {
set $upstream awesome-project:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass $upstream;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
As you can see, this server block configuration file is set to handle requests for an awesome-project.test domain. So, when a request comes from that domain, the NGINX container will look on the defined root folder for any file that matches the first location definition. Assuming that it finds one that matches one of the patterns set, it’ll then scan all of the remaining location definitions (on this example file, there’s one more) and if it can match the pattern (which is looking for any file ending in .php), then it executes the corresponding block instructions. And is in this second locationblock that all the magic happen.
First and foremost, we’re setting a container:port mapping in a variable, $upstream. This is a very important part of the configuration, which avoids NGINX to malfunction, and enter a restart loop, if the container happens to not been started yet. If that would happen, all other projects that it may be responsible to handle wouldn’t be processed!
After calcuating, and successfully resolving the project’s container, NGINX compiles the path info and request, passing it to the destination container, through the defined port. After this, the PHP container will pick up the call, execute the code and return the response back to NGINX to be outputted to the user.
Remember the Scripts folder? As I’ve said before, that folder contains several scripts I use to perform a lot of common tasks on my day-to-day work. By copying them to a bin folder, I can simulate the behaviour of any program as if it was installed on my computer, but, in fact, I’m running them in isolation, through a Docker container.
For example, the command to run Composer, which is a dependency manager for PHP, is the following:
#!/bin/sh
docker run --rm -ti --user $(id -u):$(id -g) \
--volume ~/.config/composer:/tmp \
--volume $SSH_AUTH_SOCK:/ssh-auth.sock \
--volume /etc/passwd:/etc/passwd:ro \
--volume /etc/group:/etc/group:ro \
--volume $(pwd):/app \
--env SSH_AUTH_SOCK=/ssh-auth.sock \
composer $@
With this, I can run any composer command exactly as I would if I installed it on my computer, but without needing to install PHP and all it’s required dependencies. I just do composer install or composer update and be done with the task. This script can even access my SSH keys to authenticate the requests on private repositories.
Here’s another example, this time for running Yarn, a dependency manager for Javascript based projects:
#!/bin/sh
docker run --rm -ti --user $(id -u):$(id -g) \
--volume $(pwd):/usr/src/app \
-w /usr/src/app \
node yarn $@
And I run it with a simple yarn {my command here}.
This works absolutely perfect and without any hassle whatsoever. If I end up not need a script, anymore, I simply remove it from my computer with a simple rm {script file path} and that’s it! No unnecessary dependencies lying around on my computer.
Well done! You now have the necessary information to be able to create a smooth infrastructure, just like me! You’ve learned how to put different containers to communicate with each other, how to use a single NGINX container to reverse-proxy, and serve, as many projects as you need and, on top of that, you learned that you can use containers to run your everyday scripts and not needing to worry if its installed on your current working computer.
Hope you’ve enjoyed the article. If you have any question, feel free to contact me on Twitter. I’m more than happy to help you with any difficulty you may have while applying the knowledge available on this article.
A few days back I had the need to run Deployer, a deployment tool for PHP, on an isolated container. This was to avoiding keeping it, as a dependency, on a project that needed to be constantly tested on a PHP version not supported by a version of the tool that needed to be run.
I couldn’t simply remove the dependency, because it’s used to deploy the project to production, and I didn’t want to have it installed directly, and globally (using composer), on my computer. So, I simply created a Docker image that would have that tool pre-installed and configured, and simply execute a script that would spin up a container, run the desired command, and then self-remove after the work is done.
After I got it working as expected, I thought it would be useful for others, too, so I published it on Docker Hub. You can get to the page, here.
Now you too can deploy your projects without having to install Deployer either globally on your computer or as a project dependency. The only thing you need is to run the container on the same folder where your deploy.php file is and everything should work as expected.
If you have any questions of feedback, feel free to contact me.