Roots Discourse

Deploy Trellis with Circle CI


#1

I’ve looked at Deploy via Jenkins and I think I’m running into a similar issue when deploying with Circle CI.

I have everything set up using a circle.yml file. Everything installs and builds but Circle will always run everything from the build_dir specified and as far as I can tell you can’t change directory half way through the process.

I have reached a point where the only error I’m getting is when Circle attempts to run ./bin/deploy.sh staging example.com.

I get the error Error: staging is not a valid environment (hosts/staging does not exist). but the available environments list is empty.

Do I need to somehow provision the Circle build server using ansible? I feel like this could be relevant https://github.com/roots/trellis/blob/master/hosts/development#L22

Here is my circle.yml file:

# General config
general:
  build_dir: site
  
# Configure the machine
machine:
  timezone:
    Europe/London
  node:
    version: 6.0.0
  python:
    version: 2.7.10

dependencies:
  pre:
    - pip install ansible
  override:
    - npm install
    - composer install
  
test:
  override:
    - npm run test

# Deploy
deployment:
  staging:
    branch: staging
    commands:
      - ../trellis/bin/deploy.sh staging example.com

Can someone point me in the right direction, please?


#2

Ok, I’ve figured out that you can change directories in Circle CI but you need to chain the command so cd ../trellis && ansible-playbook deploy.yml -e "site=example.com env=staging"
works.

I still needed to pass the location of the host file to ansible with -i hosts/staging

I was struggling with .vault_pass as it’s not in the repo and there is no environment variable for it (only for the location of the file itself).

I got around this by creating a file as part of the pre dependency task and echo my own env variable into the file.

dependencies:
  pre:
    - pip install ansible
    - cd ../trellis && echo "${ANSIBLE_VAULT_PASSWORD}" > .vault_pass

My full (working) circle.yml file now looks like this:

# General config
general:
  build_dir: site
  
# Configure the machine
machine:
  timezone:
    Europe/London
  node:
    version: 6.0.0
  python:
    version: 2.7.10

dependencies:
  pre:
    - pip install ansible
    - cd ../trellis && echo "${ANSIBLE_VAULT_PASSWORD}" > .vault_pass
  override:
    - npm install
    - composer install
  
test:
  override:
    - npm run test

# Deploy
deployment:
  staging:
    branch: staging
    commands:
      - cd ../trellis && ansible-playbook deploy.yml -e "site=example.com env=staging" -i hosts/staging

Reference:


#3

Now the final error I need to overcome is the following (removed the IP address):

TASK [setup] *******************************************************************
System info:
  Ansible 2.2.0.0; Linux
  Trellis at "Add myhostname to nsswitch.conf to ensure resolvable hostname"
---------------------------------------------------
Failed to connect to the host via ssh: Warning: Permanently added
'0.0.0.0 (ECDSA) to the list of known hosts.
Permission denied (publickey).

fatal: [0.0.0.0]: UNREACHABLE! => {"changed": false, "unreachable": true}

I’m deploying to AWS so it might be different for Digital Ocean.
I’m also deploying from Circle CI’s build user so that might also be a problem.

  • I’ve tried manually generating an SSH key pair and adding it to AWS and Circle CI.
  • I’ve tried importing a key pair to AWS (instead of doing it while SSH’d into the instance)
  • I’ve tried adding an IAM role and adding the id and secret to Circle CI

No matter what I do I still have the error above.

Do I need to do something with Trellis user keys?

This seems relevant; related to ssh-forwarding as I’m not deploying from my local machine.

You should not need auth tokens or private keys for the web_user. If you run into trouble cloning a remote repo during deploy, see https://developer.github.com/guides/using-ssh-agent-forwarding/ for tips and troubleshooting.


#4

Ok, I’ve added the publickey that is present in the AWS authorized_keys as a deploy key in the GitHub repo. I have also added the user .keys entry to the group_vars/all/users.yml file and re-provisioned the server.

- https://github.com/username.keys

Now I am able to get past the public key error and on to another:

TASK [deploy : Copy project local files] ***************************************
 [WARNING]: Unable to find '~/.ssh/id_rsa.pub' in expected paths.

System info:
  Ansible 2.2.0.0; Linux
  Trellis at "Add myhostname to nsswitch.conf to ensure resolvable hostname"
---------------------------------------------------
could not locate file in lookup: ~/.ssh/id_rsa.pub
fatal: [0.0.0.0]: FAILED! => {"failed": true}

At the moment I’m unsure whether this means the destination machine or the Circle CI build machine. But this might be what’s causing the issue:

keys:
  - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

#5

Ok, no closer :frowning:

Does someone with more expertise in the area of ssh keys and Trellis users have some insight into what the issue might be?


#6

if you have added

- https://github.com/username.keys

to the keys for users then you should be able to avoid the problem by commenting out (or deleting)

- "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

That ~/.ssh/id_rsa.pub is present just as a default to accommodate simpler cases where someone is just provisioning from their local machine that has ~/.ssh/id_rsa.pub. It saves some people one step of configuration. In your case, you did need the step of configuring these keys, and your configuration probably should involve omitting the reference to ~/.ssh/id_rsa.pub.


#7

@fullyint Great! Removed and that issue as been resolved.

Now I’m trying to install composer dependencies, and one is failing from a private GitHub repo.
The interesting thing is that there are no errors when Circle CI runs the install as part of provisioning the build server (which I now realise I can remove as composer is run on the destination server).

The error I’m getting doesn’t seem to have any information about a possible resolution:

[RuntimeException]
  Failed to execute git clone --no-checkout 'git@github.com:myrepo.git' 'web/app/plugins/myplugin/' --dissociate
--reference '/home/web/.cache/composer/vcs/git@github.com-myrepo.git/' && cd 'web/app/plugins/myplugin' && git remote add
composer 'git@github.com:myrepo.git' && git fetch
composer

From Googling, it appears to be the result of a 401 error. That points to Circle CI not forwarding the SSH keys so I’ll need to set them up manually for my destination server and GitHub (I guess).

I’m continuing to document this here in the hopes that someone else might find it useful. I’ll try and end with the complete resolution (if I find one).


#8

Everything is working!

I’ve finally got a successful build and deploy with Circle CI.

There are a few things needed:

  • Generate a GitHub repo deploy key with Circle CI
  • Add your SSH private key to Circle CI
  • Add your .vault_pass contents to an environment variable
  • Add tags to deploy hooks so they can be ignored
  • Configure Circle CI machines with circle.yml file

Generate a GitHub repo deploy key with Circle CI
Circle CI can create a deploy key automatically under Project Settings → Checkout SSH Keys

Add your SSH private key to Circle CI
When you connect to your server normally with Trellis, you have an SSH key pair which checks your private key on your local machine with the public key in the authorized_keys file on the server.

Since the Circle CI build machine is now taking the place of your local machine, you need to give it your local private key. You could probably generate a new key specifically for this purpose but permissions between both servers & GitHub can be overly complex.

Add your .vault_pass contents to an environment variable
Ansible Vault needs the password to decrypt the files but the .vault_pass file is kept outside of version control. So we need to create one at build time by adding a custom environment variable and echoing it to a file once Circle CI has spun up.

echo "${ANSIBLE_VAULT_PASSWORD}" > .vault_pass

This is done in the circle.yml file below after we change into the Trellis directory.

Add tags to deploy hooks so they can be ignored
There is some duplication that goes on during this configuration. Before testing my code, The build is run using Gulp + NPM. We don’t want to build it, run tests, then build it again on the CI. But we do want to be able to build it locally in my Vagrant VM.

To overcome this we add tags to the tasks in the before-build.yml playbook and simply tell Ansible to skip tasks with the tag when calling the deploy playbook.

This is my build-before.yml playbook.

- name: Run npm install
  command: npm install
  connection: local
  args:
    chdir: "{{ project.local_path }}/web/app"
  tags: skip-using-ci

- name: Rebuild Node Sass
  command: npm rebuild node-sass
  connection: local
  args:
    chdir: "{{ project.local_path }}/web/app"
  tags: skip-using-ci

- name: Run gulp
  command: npm run production
  connection: local
  args:
    chdir: "{{ project.local_path }}/web/app"
  tags: skip-using-ci

- name: Copy project local files
  synchronize:
    src: "{{ project.local_path }}/web/app/themes"
    dest: "{{ deploy_helper.new_release_path }}/web/app/themes"
    group: no
    owner: no
    rsync_opts: --chmod=Du=rwx,--chmod=Dg=rx,--chmod=Do=rx,--chmod=Fu=rw,--chmod=Fg=r,--chmod=Fo=r

As you can see, some of them have the tag skip-using-ci. When we call the deploy playbook we can add --skip-tags "skip-using-ci" to, well, skip them.

Configure Circle CI machines
Using the circle.yml file you need to configure the environment under which everything will run.

# Set the folder under which commands will be executed
general:
  build_dir: site
  
# Configure the machine
machine:
  timezone:
    Europe/London
  node:
    version: 6.0.0
  python:
    version: 2.7.10

dependencies:
  pre:
    # Allow ansible playbooks to be run
    - pip install ansible
    # Turn an environment variable into a vault file that ansible can read
    - cd ../trellis && echo "${ANSIBLE_VAULT_PASSWORD}" > .vault_pass
  # This _should_ be inferred by Circle CI as we specify a node version but for some reason, it never ran for me. This tells it explicitly to run.
  override:
    - npm install
  
test:
  # Again, this _should_ be inferred by Circle CI but wasn't.
  override:
    - npm run test

# Deployment Scenarios
deployment:
  staging:
    branch: staging
    commands:
      # This is just an ansible playbook command with some options to specify where the host file is located and that we want to skip tasks with the tag `skip-using-ci`
      - cd ../trellis && ansible-playbook deploy.yml -e "site=example.com env=staging" -i hosts/staging --skip-tags "skip-using-ci" -vvv

I hope this helps. It will probably work with Travis as well. Not too sure about Jenkins.