virtual-machine.jinja (383 lines of code) (raw):
{% set COMPUTE_URL_BASE = 'https://www.googleapis.com/compute/v1/' %}
{% set BASE_NAME = properties['baseName'] + '-' + env['name'] %}
{% set REGION = properties['zone'][:-2] %}
{% set DISK_SIZE = properties['zone'][:-2] %}
{% set GOOGLE_FACTORY = '1.0.16' %}
{% set NGINX_IMAGE = 'jwilder/nginx-proxy' %}
{% set LETSENCRYPT_IMAGE = 'jrcs/letsencrypt-nginx-proxy-companion' %}
{% set NGINX = 'nginx' %}
{% set LETSENCRYPT = 'letsencrypt' %}
{%- if properties['enableHttps'] %}
{% set SERVER_URL = 'https://' + properties['domainName'] %}
{%- else %}
{% set SERVER_URL = 'http://${COREOS_GCE_HOSTNAME}' %}
{%- endif %}
{% if properties['size'] == 'small' %}
{% set DISK_SIZE = 30 %}
{% set DISK_TYPE = 'pd-standard' %}
{% set MACHINE_TYPE = 'custom-1-3072' %}
{% elif properties['size'] == 'large' %}
{% set DISK_SIZE = 100 %}
{% set DISK_TYPE = 'pd-ssd' %}
{% set MACHINE_TYPE = 'custom-4-8192' %}
{% else %}
{% set DISK_SIZE = 50 %}
{% set DISK_TYPE = 'pd-ssd' %}
{% set MACHINE_TYPE = 'custom-2-4096' %}
{% endif %}
{% macro GlobalComputeUrl(project, collection, name) -%}
{{ COMPUTE_URL_BASE }}projects/{{ project }}/global/{{ collection }}/{{ name }}
{%- endmacro %}
{% macro ZonalComputeUrl(project, zone, collection, name) -%}
{{ COMPUTE_URL_BASE }}projects/{{ project }}/zones/{{ zone }}/{{ collection }}/{{ name }}
{%- endmacro %}
{% macro RegionalComputeUrl(project, region, collection, name) -%}
{{ COMPUTE_URL_BASE }}projects/{{ project }}/regions/{{ region }}/{{ collection }}/{{ name }}
{%- endmacro %}
{% macro SqlInstanceName(project, region, dbName) -%}
{{ project }}:{{ region }}:{{ dbName }}
{%- endmacro %}
resources:
- name: {{ BASE_NAME }}-data
type: compute.v1.disk
properties:
zone: {{ properties["zone"] }}
sizeGb: {{ DISK_SIZE }}
type: {{ ZonalComputeUrl(env['project'], properties['zone'], 'diskTypes', DISK_TYPE) }}
{%- if not properties['ipAddress'] %}
- name: {{ BASE_NAME }}-address
type: compute.v1.address
properties:
name: {{ BASE_NAME }}-address
region: {{ REGION }}
{%- endif %}
- name: {{ BASE_NAME }}
type: compute.v1.instance
properties:
zone: {{ properties['zone'] }}
machineType: {{ ZonalComputeUrl(env['project'], properties['zone'], 'machineTypes', MACHINE_TYPE) }}
serviceAccounts:
- email: {{ properties['serviceAccount'] }}
scopes:
- "https://www.googleapis.com/auth/cloud-platform"
metadata:
items:
- key: user-data
value: |
#cloud-config
write_files:
- path: "/etc/teamcity/version"
content: |
TEAMCITY_VERSION={{ properties['teamcityVersion'] }}
- path: "/etc/teamcity/database.properties"
content: |
# Database: MySQL
connectionUrl=jdbc:mysql://google/{{ properties['database']['name'] }}?cloudSqlInstance={{ env['project'] }}:{{ REGION }}:{{ properties['database']['instance'] }}&socketFactory=com.google.cloud.sql.mysql.SocketFactory&useSSL=false
connectionProperties.user={{ properties['database']['user'] }}
maxConnections=50
testOnBorrow=true
- path: "/etc/teamcity/disabled-plugins.xml"
content: |
<?xml version="1.0" encoding="UTF-8"?>
<disabled-plugins>
<disabled-plugin name="cloud-amazon" />
<disabled-plugin name="s3-artifact-storage" />
<disabled-plugin name="vsphere" />
</disabled-plugins>
- path: "/etc/flatcar/update.conf"
content: |
REBOOT_STRATEGY="off"
{%- if properties['enableHttps'] %}
- path: "/etc/teamcity/checkCertificate.sh"
permissions: 0755
content: |
#!/bin/bash
attempt=0
while docker exec {{ LETSENCRYPT }} /app/cert_status | grep {{ properties['domainName'] }} > /dev/null; [[ $? -ne 0 ]]; do
retry_in_mins=$((3**$attempt))
echo "Will try to renew certificate for {{ SERVER_URL }} in $retry_in_mins minutes"
sleep $((60 * $retry_in_mins))
echo "Trying to renew certificate for {{ SERVER_URL }}..."
docker exec {{ LETSENCRYPT }} /app/signal_le_service
sleep 30
attempt=$(($attempt + 1))
done
{% endif %}
coreos:
units:
- name: "format-mnt-data.service"
enable: true
content: |
[Unit]
Requires=network-online.target
Before=teamcity-server.service mnt-data.mount
ConditionPathExists=!/dev/mapper/app-data
[Service]
Type=oneshot
ExecStart=/bin/bash -c \
'/usr/sbin/pvcreate /dev/disk/by-id/google-{{ BASE_NAME }}-data && \
/usr/sbin/vgcreate app /dev/disk/by-id/google-{{ BASE_NAME }}-data && \
/usr/sbin/lvcreate -l 100%%FREE -n data app && \
/usr/sbin/mkfs.ext4 /dev/mapper/app-data'
[Install]
WantedBy=multi-user.target
- name: "mnt-data.mount"
enable: true
content: |
[Unit]
Before=teamcity-server.service
After=format-mnt-data.service
Requires=format-mnt-data.service
ConditionVirtualization=!container
Conflicts=umount.target
[Mount]
What=/dev/mapper/app-data
Where=/mnt/data
Type=ext4
Options=
[Install]
RequiredBy=teamcity-server.service
- name: "get-mysql-connector.service"
enable: true
content: |
[Unit]
Before=teamcity-server.service
After=mnt-data.mount docker.service
Requires=mnt-data.mount docker.service
ConditionPathExists=!/mnt/data/teamcity-server/data/lib/jdbc/
[Service]
Type=oneshot
ExecStart=/bin/mkdir -p /mnt/data/teamcity-server/data/lib/jdbc
ExecStart=/bin/wget -O /mnt/data/teamcity-server/data/lib/jdbc/mysql-socket-factory.jar \
https://repo.maven.apache.org/maven2/com/google/cloud/sql/mysql-socket-factory/{{ GOOGLE_FACTORY }}/mysql-socket-factory-{{ GOOGLE_FACTORY }}.jar
ExecStart=/bin/wget -O pom.xml https://repo.maven.apache.org/maven2/com/google/cloud/sql/mysql-socket-factory/{{ GOOGLE_FACTORY }}/mysql-socket-factory-{{ GOOGLE_FACTORY }}.pom
ExecStart=/bin/bash -c 'docker run --rm -v "$(pwd)":/opt/maven -v /mnt/data/teamcity-server/data/lib/jdbc:/opt/maven/target/dependency -w /opt/maven maven:alpine mvn dependency:copy-dependencies && docker rmi maven:alpine'
ExecStart=/bin/rm pom.xml
[Install]
WantedBy=multi-user.target
- name: "get-google-plugins.service"
enable: true
content: |
[Unit]
Before=teamcity-server.service
After=mnt-data.mount
Requires=mnt-data.mount
ConditionPathExists=!/mnt/data/teamcity-server/data/plugins/google-plugins.txt
[Service]
Type=oneshot
ExecStart=/bin/mkdir -p /mnt/data/teamcity-server/data/plugins
ExecStart=/bin/curl https://raw.githubusercontent.com/JetBrains/teamcity-google-template/master/google-plugins.txt \
-o /mnt/data/teamcity-server/data/plugins/google-plugins.txt
ExecStart=/bin/bash -c 'cd /mnt/data/teamcity-server/data/plugins && curl -K google-plugins.txt'
[Install]
WantedBy=multi-user.target
- name: "prepare-config.service"
enable: true
content: |
[Unit]
Before=teamcity-server.service
After=mnt-data.mount
Requires=mnt-data.mount network-online.target
ConditionPathExists=!/mnt/data/teamcity-server/data/config
[Service]
Type=oneshot
ExecStart=/bin/mkdir -p /mnt/data/teamcity-server/data/config
ExecStart=/bin/mv /etc/teamcity/database.properties /mnt/data/teamcity-server/data/config/
ExecStart=/bin/mv /etc/teamcity/disabled-plugins.xml /mnt/data/teamcity-server/data/config/
[Install]
WantedBy=multi-user.target
- name: "teamcity-server.service"
command: "start"
content: |
[Unit]
Description=TeamCity Server
After=docker.service mnt-data.mount get-mysql-connector.service get-google-plugins.service prepare-config.service
Requires=docker.service mnt-data.mount get-mysql-connector.service get-google-plugins.service prepare-config.service
[Service]
TimeoutStartSec=1200s
EnvironmentFile=/etc/teamcity/version
{%- raw %}
ExecStartPre=/bin/sh -c "docker images --filter 'before=jetbrains/teamcity-server:${TEAMCITY_VERSION}' --format '{{.ID}} {{.Repository}}' | grep 'jetbrains/teamcity-server' | grep -Eo '^[^ ]+' | xargs -r docker rmi"
{%- endraw %}
ExecStartPre=/usr/bin/docker create \
-e TEAMCITY_SERVER_MEM_OPTS="-Xmx$(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 2))k -XX:MaxPermSize=270m -XX:ReservedCodeCacheSize=350m" \
{%- if properties['enableHttps'] %}
-e VIRTUAL_PORT=8111 \
-e VIRTUAL_HOST={{ properties['domainName'] }} \
-e LETSENCRYPT_HOST={{ properties['domainName'] }} \
{%- if properties['domainOwnerEmail'] %}
-e LETSENCRYPT_EMAIL={{ properties['domainOwnerEmail'] }} \
{%- endif %}
{%- else %}
-p 80:8111 \
{%- endif %}
-v /mnt/data/teamcity-server/data:/data/teamcity_server/datadir \
-v /mnt/data/teamcity-server/logs:/opt/teamcity/logs \
-v /mnt/resource/teamcity-server/temp:/opt/teamcity/temp \
--name teamcity-server \
jetbrains/teamcity-server:${TEAMCITY_VERSION}
ExecStartPre=/bin/sh -c "echo 'gcp' > dist && docker cp dist teamcity-server:/opt/teamcity/webapps/ROOT/WEB-INF/DistributionType.txt && rm dist"
ExecStart=/usr/bin/docker start teamcity-server -a
ExecStop=-/usr/bin/docker exec teamcity-server /opt/teamcity/bin/teamcity-server.sh stop 60
ExecStopPost=-/usr/bin/docker stop teamcity-server
ExecStopPost=-/usr/bin/docker rm teamcity-server
Restart=always
[Install]
WantedBy=multi-user.target
- name: "teamcity-agent.service"
command: "start"
content: |
[Unit]
Description=TeamCity Agent
After=teamcity-server.service coreos-metadata.service
Requires=teamcity-server.service coreos-metadata.service
[Service]
TimeoutStartSec=1200s
EnvironmentFile=/etc/teamcity/version
EnvironmentFile=/run/metadata/coreos
{%- raw %}
ExecStartPre=/bin/sh -c "docker images --filter 'before=jetbrains/teamcity-agent:${TEAMCITY_VERSION}' --format '{{.ID}} {{.Repository}}' | grep 'jetbrains/teamcity-agent' | grep -Eo '^[^ ]+' | xargs -r docker rmi" %}
{%- endraw %}
ExecStart=/usr/bin/docker run \
-v /mnt/data/teamcity-agent/conf:/opt/buildagent/conf \
-v /mnt/data/teamcity-agent/logs:/opt/buildagent/logs \
-v /mnt/data/teamcity-agent/plugins:/opt/buildagent/plugins \
-v /mnt/data/teamcity-agent/system:/opt/buildagent/system \
-v /mnt/resource/teamcity-agent/temp:/opt/buildagent/temp \
-v /mnt/resource/teamcity-server/temp:/opt/teamcity/temp \
-v /mnt/data/teamcity-agent/tools:/opt/buildagent/tools \
--privileged \
-e DOCKER_IN_DOCKER=start \
-e SERVER_URL={{ SERVER_URL }} \
-e AGENT_NAME=Default \
--name teamcity-agent \
jetbrains/teamcity-agent:${TEAMCITY_VERSION}
ExecStop=-/usr/bin/docker exec teamcity-agent /opt/buildagent/bin/agent.sh stop
ExecStopPost=-/usr/bin/docker stop teamcity-agent
ExecStopPost=-/usr/bin/docker rm teamcity-agent
Restart=always
[Install]
WantedBy=multi-user.target
{%- if properties['enableHttps'] %}
- name: "{{ NGINX }}-directories.service"
enable: true
content: |
[Unit]
Before={{ NGINX }}.service
After=mnt-data.mount docker.service
Requires=mnt-data.mount docker.service
ConditionPathExists=!/opt/domains/vhost.d/default
[Service]
Type=oneshot
ExecStart=/bin/mkdir -p /opt/domains/certs
ExecStart=/bin/mkdir -p /opt/domains/vhost.d
ExecStart=/bin/mkdir -p /opt/domains/nginx/html
ExecStart=/bin/sh -c "echo 'proxy_read_timeout 1200;' >> /opt/domains/vhost.d/default"
ExecStart=/bin/sh -c "echo 'proxy_connect_timeout 240;' >> /opt/domains/vhost.d/default"
ExecStart=/bin/sh -c "echo 'client_max_body_size 0;' >> /opt/domains/vhost.d/default"
[Install]
WantedBy=multi-user.target
- name: "{{ NGINX }}.service"
command: "start"
content: |
[Unit]
Description=NGINX reverse proxy
Requires=docker.service {{ NGINX }}-directories.service
[Service]
TimeoutStartSec=1200s
ExecStart=/usr/bin/docker run \
-p 80:80 \
-p 443:443 \
-v /opt/domains/certs:/etc/nginx/certs:ro \
-v /opt/domains/vhost.d:/etc/nginx/vhost.d \
-v /opt/domains/nginx/html:/usr/share/nginx/html \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
--name {{ NGINX }} \
{{ NGINX_IMAGE }}
ExecStop=-/usr/bin/docker stop {{ NGINX }}
ExecStopPost=-/usr/bin/docker rm {{ NGINX }}
Restart=always
[Install]
WantedBy=multi-user.target
- name: "{{ LETSENCRYPT }}.service"
command: "start"
content: |
[Unit]
Description=Let's Encrypt
Requires=docker.service
After={{ NGINX }}.service
Before=teamcity-server.service
[Service]
TimeoutStartSec=1200s
ExecStart=/usr/bin/docker run \
-v /opt/domains/certs:/etc/nginx/certs:rw \
--volumes-from {{ NGINX }} \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--name {{ LETSENCRYPT }} \
{{ LETSENCRYPT_IMAGE }}
ExecStop=-/usr/bin/docker stop {{ LETSENCRYPT }}
ExecStopPost=-/usr/bin/docker rm {{ LETSENCRYPT }}
Restart=always
[Install]
WantedBy=multi-user.target
{%- endif %}
- name: "report-waiter-status.service"
enable: true
command: "start"
content: |
[Unit]
{%- if properties['enableHttps'] %}
Requires=docker.service teamcity-server.service {{ NGINX }}.service {{ LETSENCRYPT }}.service
{%- else %}
Requires=docker.service teamcity-server.service
{%- endif %}
Before=teamcity-agent.service
ConditionPathExists=!/etc/teamcity/waiter
[Service]
Type=oneshot
EnvironmentFile=/run/metadata/coreos
{%- if properties['enableHttps'] %}
ExecStartPre=/bin/bash /etc/teamcity/checkCertificate.sh
{%- endif %}
ExecStartPre=/bin/bash -c "until $(curl --output /dev/null --silent --fail {{ SERVER_URL }}); do echo 'Waiting for success response from {{ SERVER_URL }}'; sleep 10; done;"
ExecStart=/usr/bin/docker run --rm -e "CLOUDSDK_CORE_DISABLE_PROMPTS=1" google/cloud-sdk:alpine gcloud beta runtime-config configs variables set success/{{ properties['waiter']['name'] }} success --config-name {{ properties['waiter']['config'] }}
ExecStartPost=/usr/bin/docker rmi google/cloud-sdk:alpine
ExecStartPost=/bin/touch /etc/teamcity/waiter
[Install]
WantedBy=multi-user.target
disks:
- deviceName: boot
type: PERSISTENT
autoDelete: true
boot: true
initializeParams:
diskName: {{ BASE_NAME }}-disk
sourceImage: {{ GlobalComputeUrl('teamcitytest-166414', 'images', 'flatcar-stable') }}
diskSizeGb: 14
- deviceName: {{ BASE_NAME }}-data
type: PERSISTENT
autoDelete: true
source: $(ref.{{ BASE_NAME }}-data.selfLink)
networkInterfaces:
- accessConfigs:
- name: external-nat
type: ONE_TO_ONE_NAT
{%- if properties['ipAddress'] %}
natIP: {{ properties['ipAddress'] }}
{%- else %}
natIP: $(ref.{{ BASE_NAME }}-address.address)
{%- endif %}
network: {{ GlobalComputeUrl(env['project'], 'networks', properties['network']) }}
{%- if properties['subnetwork'] %}
subnetwork: {{ RegionalComputeUrl(env['project'], REGION, 'subnetworks', properties['subnetwork']) }}
{%- endif %}
tags:
items:
- http-server
{%- if properties['enableHttps'] %}
- https-server
{%- endif %}
- ssh-server
outputs:
- name: ip
value: $(ref.{{ BASE_NAME }}.networkInterfaces[0].accessConfigs[0].natIP)